//! A lightweight layer on top of [Hyper](https://hyper.rs/)
//! to facilitate building web applications.
use std::collections::HashMap;
pub use error::Error;
pub use mime;
use cookie::Cookie;
use header::HeaderName;
use mime::{APPLICATION_WWW_FORM_URLENCODED, Mime};
use serde::{Deserialize, de::DeserializeOwned};
use hyper::{Body, StatusCode, body::Bytes, header::{self, HeaderValue}, http::request::Parts};
use time::{Duration, OffsetDateTime};
pub use httpdate;
type HyperRequest = hyper::Request
;
type HyperResponse = hyper::Response;
pub mod security;
mod error;
mod signed;
/// Convenience wrapper around [`hyper::Request`].
pub struct Request {
body: Body,
parts: Parts,
cookies: Option>>,
}
impl From for Request {
fn from(req: HyperRequest) -> Self {
let (parts, body) = req.into_parts();
Request{parts, body, cookies: None}
}
}
impl Into for Response {
fn into(self) -> HyperResponse {
self.res
}
}
fn enforce_content_type(req: &Parts, mime: Mime) -> Result<(),Error> {
let received_type = req.headers.get(header::CONTENT_TYPE).ok_or(Error::bad_request(format!("expected content-type: {}", mime)))?;
if *received_type != mime.to_string() {
return Err(Error::bad_request(format!("expected content-type: {}", mime)))
}
Ok(())
}
#[derive(Deserialize)]
struct CsrfData {
csrf: String,
}
impl Request {
pub fn cookies(&mut self) -> &HashMap {
if let Some(ref cookies) = self.cookies {
return cookies
}
let mut cookies = HashMap::new();
for header in self.parts.headers.get_all(header::COOKIE) {
let raw_str = match std::str::from_utf8(header.as_bytes()) {
Ok(string) => string,
Err(_) => continue
};
for cookie_str in raw_str.split(';').map(|s| s.trim()) {
if let Ok(cookie) = Cookie::parse_encoded(cookie_str) {
cookies.insert(cookie.name().to_string(), cookie.into_owned());
}
}
}
self.cookies = Some(cookies);
&self.cookies.as_ref().unwrap()
}
pub fn method(&self) -> &hyper::Method {
&self.parts.method
}
pub fn uri(&self) -> &hyper::Uri {
&self.parts.uri
}
/// 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
/// [`parse_form_csrf()`] instead.
///
/// # Example
///
/// ```
/// use hyper::{Response, Body};
/// use sputnik::{Request, Error};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Message {text: String, year: i64}
///
/// async fn greet(req: &mut Request) -> Result, Error> {
/// let msg: Message = req.into_form().await?;
/// Ok(Response::new(format!("hello {}", msg.text).into()))
/// }
/// ```
pub async fn into_form(&mut self) -> Result {
enforce_content_type(&self.parts, APPLICATION_WWW_FORM_URLENCODED)?;
let full_body = self.into_body().await?;
serde_urlencoded::from_bytes::(&full_body).map_err(|e|Error::bad_request(e.to_string()))
}
/// 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, Response, Error};
/// use sputnik::security::CsrfToken;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Message {text: String}
///
/// async fn greet(req: &mut Request) -> Result {
/// let mut response = Response::new();
/// let csrf_token = CsrfToken::from_request(req, &mut response);
/// *response.body() = match (req.method()) {
/// &Method::GET => format!("", csrf_token.html_input()).into(),
/// &Method::POST => {
/// let msg: Message = req.into_form_csrf(&csrf_token).await?;
/// format!("hello {}", msg.text).into()
/// },
/// _ => return Err(Error::method_not_allowed("only GET and POST allowed".to_owned())),
/// };
/// Ok(response)
/// }
/// ```
pub async fn into_form_csrf(&mut self, csrf_token: &security::CsrfToken) -> Result {
enforce_content_type(&self.parts, APPLICATION_WWW_FORM_URLENCODED)?;
let full_body = self.into_body().await?;
let csrf_data = serde_urlencoded::from_bytes::(&full_body).map_err(|_|Error::bad_request("no csrf token".to_string()))?;
csrf_token.matches(csrf_data.csrf)?;
serde_urlencoded::from_bytes::(&full_body).map_err(|e|Error::bad_request(e.to_string()))
}
pub async fn into_body(&mut self) -> Result {
hyper::body::to_bytes(&mut self.body).await.map_err(|_|Error::internal("failed to read body".to_string()))
}
/// Parses the query string of the request into a given struct.
pub fn query(&self) -> Result {
serde_urlencoded::from_str::(self.parts.uri.query().unwrap_or("")).map_err(|e|Error::bad_request(e.to_string()))
}
}
/// Convenience wrapper around [`hyper::Response`].
pub struct Response {
res: HyperResponse
}
impl Response {
pub fn new() -> Self {
Response{res: HyperResponse::new(Body::empty())}
}
pub fn status(&mut self) -> &mut StatusCode {
self.res.status_mut()
}
pub fn body(&mut self) -> &mut Body {
self.res.body_mut()
}
pub fn headers(&mut self) -> &mut hyper::HeaderMap {
self.res.headers_mut()
}
pub fn set_header>(&mut self, header: HeaderName, value: S) {
self.res.headers_mut().insert(header, HeaderValue::from_str(value.as_ref()).unwrap());
}
pub fn set_content_type(&mut self, mime: mime::Mime) {
self.res.headers_mut().insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap());
}
pub fn redirect>(&mut self, location: S, code: StatusCode) {
*self.res.status_mut() = code;
self.set_header(header::LOCATION, location);
}
pub fn set_cookie(&mut self, cookie: Cookie) {
self.res.headers_mut().append(header::SET_COOKIE, cookie.encoded().to_string().parse().unwrap());
}
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);
}
}