use cookies::Cookies;
use httparse::{Header, Request as RawRequest};
use params::Params;
use path::PathIterator;
use std::{borrow::Cow, convert::Infallible, fmt::Debug, future::Future, io::Read, ops::Index};
pub mod cookies;
pub mod params;
pub mod path;
pub mod percent_decode;
pub fn canonical_reason(code: u16) -> &'static str {
match code {
200 => "OK",
204 => "No Content",
304 => "Not Modified",
307 => "Temporary Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
408 => "Request Timeout",
413 => "Content Too Large",
431 => "Request Header Fields Too Large",
500 => "Internal Server Error",
_ => "",
}
}
#[derive(Clone, Copy, Debug)]
pub struct Request<'headers, 'buf> {
pub method: &'buf str,
pub path: &'buf str,
pub query: Option<&'buf str>,
pub version: u8,
pub headers: &'headers [Header<'buf>],
pub body: &'buf [u8],
}
impl<'headers, 'buf> Request<'headers, 'buf> {
pub fn new_from_raw(raw: RawRequest<'headers, 'buf>, body: &'buf [u8]) -> Self {
let raw_path = raw.path.unwrap();
let mut path = raw_path.split('?');
Self {
method: raw.method.unwrap(),
path: path.next().unwrap_or(raw_path),
query: path.next(),
version: raw.version.unwrap(),
headers: raw.headers,
body,
}
}
pub fn new(
method: &'buf str,
path: &'buf str,
headers: &'headers [Header<'buf>],
body: &'buf [u8],
) -> Self {
let mut spath = path.split('?');
Self {
method,
path: spath.next().unwrap_or(path),
query: spath.next(),
version: 1,
headers,
body,
}
}
#[inline]
pub fn find_header(&self, name: &str) -> Option<&'buf [u8]> {
self.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case(name))
.map(|h| h.value)
}
#[inline]
pub fn cookies(&self) -> Cookies<'buf> {
Cookies::new(self.find_header("Cookie"))
}
#[inline]
pub fn params(&self) -> Params<'buf> {
Params::new(self.query.unwrap_or(""))
}
#[inline]
pub fn url(&self) -> PathIterator<'buf> {
PathIterator::new(self.path)
}
}
#[derive(Debug, Clone, Default)]
pub struct Headers {
inner: Vec<(Cow<'static, str>, Cow<'static, str>)>,
}
impl Headers {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn push(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) {
self.inner.push((name.into(), value.into()));
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.inner.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
}
#[inline]
pub fn retain<F>(&mut self, mut f: F)
where
F: FnMut(&str, &str) -> bool,
{
self.inner.retain(|(k, v)| f(k, v));
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl Index<usize> for Headers {
type Output = (Cow<'static, str>, Cow<'static, str>);
fn index(&self, index: usize) -> &Self::Output {
&self.inner[index]
}
}
#[derive(Debug)]
pub struct Response {
pub status: u16,
pub headers: Headers,
pub body: Option<Body>,
}
impl Response {
#[inline]
pub fn new_with_code(status: u16) -> Self {
Self {
status,
headers: Headers::new(),
body: None,
}
}
pub fn new_with_body(
body: impl Into<Body>,
content_type: Option<impl Into<Cow<'static, str>>>,
) -> Self {
let headers = if let Some(content_type) = content_type {
Headers {
inner: vec![("Content-Type".into(), content_type.into())],
}
} else {
Headers::new()
};
Self {
status: 200,
headers,
body: Some(body.into()),
}
}
#[inline]
pub fn empty() -> Self {
Self::new_with_code(204)
}
#[inline]
pub fn ok() -> Self {
Self::new_with_code(200)
}
#[inline]
pub fn not_modified(content_type: Option<impl Into<Cow<'static, str>>>) -> Self {
let mut resp = Self::new_with_code(304);
if let Some(ct) = content_type {
resp.set_header("Content-Type", ct);
}
resp
}
#[inline]
pub fn temporary_redirect(location: impl Into<Cow<'static, str>>) -> Self {
Self::new_with_code(307).with_header("Location", location)
}
#[inline]
pub fn not_found() -> Self {
Self::new_with_code(404)
}
#[inline]
pub fn method_not_allowed() -> Self {
Self::new_with_code(405)
}
#[inline]
pub fn internal_server_error() -> Self {
Self::new_with_code(500)
}
#[inline]
pub fn bad_request() -> Self {
Self::new_with_code(400)
}
#[inline]
pub fn unauthorized() -> Self {
Self::new_with_code(401)
}
#[inline]
pub fn forbidden() -> Self {
Self::new_with_code(403)
}
#[inline]
pub fn request_timeout() -> Self {
Self::new_with_code(408)
}
#[inline]
pub fn content_too_large() -> Self {
Self::new_with_code(413)
}
#[inline]
pub fn headers_too_large() -> Self {
Self::new_with_code(431)
}
#[inline]
pub fn with_body(
mut self,
body: impl Into<Body>,
content_type: Option<impl Into<Cow<'static, str>>>,
) -> Self {
match content_type {
Some(v) => {
self.set_header("Content-Type", v);
}
None => {
self.headers
.retain(|n, _| !n.eq_ignore_ascii_case("Content-Type"));
}
}
self.body = Some(body.into());
self
}
#[inline]
pub fn with_header(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
let name = name.into();
if !self
.headers
.iter()
.any(|(n, _)| n.eq_ignore_ascii_case(name.as_ref()))
{
self.headers.push(name, value);
}
self
}
pub fn set_header(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Option<Cow<'static, str>> {
let name = name.into();
let old_value = self
.headers
.inner
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name.as_ref()))
.map(|(_, v)| v.clone());
if old_value.is_some() {
self.headers
.retain(|n, _| !n.eq_ignore_ascii_case(name.as_ref()));
}
self.headers.push(name, value);
old_value
}
}
impl From<()> for Response {
fn from(_: ()) -> Response {
Response::empty()
}
}
impl From<Body> for Response {
fn from(body: Body) -> Response {
Response::new_with_body(body, Body::DEFAULT_CONTENT_TYPE)
}
}
impl From<(Body, &'static str)> for Response {
fn from((body, content_type): (Body, &'static str)) -> Response {
Response::new_with_body(body, Some(content_type))
}
}
impl From<(Body, Option<&'static str>)> for Response {
fn from((body, content_type): (Body, Option<&'static str>)) -> Response {
Response::new_with_body(body, content_type)
}
}
impl From<(Body, String)> for Response {
fn from((body, content_type): (Body, String)) -> Response {
Response::new_with_body(body, Some(content_type))
}
}
impl From<(Body, Option<String>)> for Response {
fn from((body, content_type): (Body, Option<String>)) -> Response {
Response::new_with_body(body, content_type)
}
}
impl<T: Into<Response>> From<Option<T>> for Response {
fn from(val: Option<T>) -> Response {
match val {
Some(val) => val.into(),
None => Response::not_found(),
}
}
}
impl<T: Into<Response>, E: Into<Response>> From<Result<T, E>> for Response {
fn from(val: Result<T, E>) -> Response {
match val {
Ok(val) => val.into(),
Err(err) => err.into(),
}
}
}
impl From<Infallible> for Response {
fn from(_: Infallible) -> Response {
unreachable!("Infallible cannot be converted to Response")
}
}
pub enum Body {
Immediate(Vec<u8>),
Stream {
data: Box<dyn Read + Send + 'static>,
len: Option<u64>,
},
}
impl Body {
pub const HTML: Option<&'static str> = Some("text/html; charset=utf-8");
pub const JSON: Option<&'static str> = Some("application/json");
pub const TEXT: Option<&'static str> = Some("text/plain; charset=utf-8");
pub const DEFAULT_CONTENT_TYPE: Option<&'static str> = None;
pub fn from_vec(data: impl Into<Vec<u8>>) -> Self {
Self::Immediate(data.into())
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> Option<u64> {
match self {
Body::Immediate(data) => Some(data.len() as u64),
Body::Stream { len, .. } => *len,
}
}
}
impl From<Vec<u8>> for Body {
fn from(data: Vec<u8>) -> Self {
Body::Immediate(data)
}
}
impl From<String> for Body {
fn from(data: String) -> Self {
Body::Immediate(data.into_bytes())
}
}
impl From<&str> for Body {
fn from(data: &str) -> Self {
Body::Immediate(data.as_bytes().to_vec())
}
}
impl From<&[u8]> for Body {
fn from(data: &[u8]) -> Self {
Body::Immediate(data.to_vec())
}
}
impl From<Box<[u8]>> for Body {
fn from(data: Box<[u8]>) -> Self {
Body::Immediate(data.into())
}
}
impl From<std::fs::File> for Body {
fn from(file: std::fs::File) -> Self {
let len = file.metadata().map(|meta| meta.len()).ok();
Body::Stream {
data: Box::new(file),
len,
}
}
}
impl Debug for Body {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Immediate(v) => write!(f, "Body(Immediate, len={})", v.len()),
Self::Stream { data: _, len } => write!(f, "Body(Stream, len={len:?})"),
}
}
}
pub trait FromRequest<'a, 'b, S>: Sized {
type Error: Into<Response>;
fn from_request(
req: Request<'a, 'b>,
state: &'static S,
) -> impl Future<Output = Result<Self, Self::Error>>;
}
pub trait FromBody<'a>: Sized {
type Error: Into<Response>;
fn from_body(body: &'a [u8]) -> Result<Self, Self::Error>;
}
impl<'a, 'b, S, T> FromRequest<'a, 'b, S> for T
where
T: FromBody<'b>,
{
type Error = T::Error;
async fn from_request(req: Request<'a, 'b>, _state: &'static S) -> Result<Self, Self::Error> {
T::from_body(req.body)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_add_header() {
let response = Response {
status: 200,
headers: Headers::new(),
body: None,
};
let response = response.with_header("Content-Type", "text/html");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].0, "Content-Type");
assert_eq!(response.headers[0].1, "text/html");
let response = response.with_header("content-type", "application/json");
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].0, "Content-Type");
assert_eq!(response.headers[0].1, "text/html");
let response = response.with_header("X-Custom", "value");
assert_eq!(response.headers.len(), 2);
}
#[test]
fn test_response_set_header() {
let mut response = Response {
status: 200,
headers: Headers {
inner: vec![("Content-Type".into(), "text/html".into())],
},
body: None,
};
let old_value = response.set_header("Content-Type", "application/json");
assert_eq!(old_value, Some(Cow::Borrowed("text/html")));
assert_eq!(response.headers[0].1, "application/json");
let old_value = response.set_header("X-New", "new-value");
assert_eq!(old_value, None);
assert_eq!(response.headers.len(), 2);
}
#[test]
fn test_response_set_header_removes_duplicates() {
let mut response = Response {
status: 200,
headers: Headers {
inner: vec![
("X-Custom".into(), "v1".into()),
("X-Custom".into(), "v2".into()),
],
},
body: None,
};
let old_value = response.set_header("X-Custom", "v3");
assert_eq!(old_value, Some(Cow::Borrowed("v1")));
assert_eq!(response.headers.len(), 1);
assert_eq!(response.headers[0].0, "X-Custom");
assert_eq!(response.headers[0].1, "v3");
}
#[test]
fn test_request_find_header() {
let headers = [
Header {
name: "Content-Type",
value: b"text/plain",
},
Header {
name: "X-Custom",
value: b"custom",
},
];
let req = Request::new("GET", "/", &headers, &[]);
assert_eq!(
req.find_header("Content-Type"),
Some(b"text/plain" as &[u8])
);
assert_eq!(
req.find_header("content-type"),
Some(b"text/plain" as &[u8])
);
assert_eq!(req.find_header("X-Custom"), Some(b"custom" as &[u8]));
assert_eq!(req.find_header("x-custom"), Some(b"custom" as &[u8]));
assert_eq!(req.find_header("NotFound"), None);
}
#[test]
fn test_request_path() {
let headers = [];
let req = Request::new("GET", "/path?query=1", &headers, &[]);
assert_eq!(req.path, "/path");
let req = Request::new("GET", "/path", &headers, &[]);
assert_eq!(req.path, "/path");
let req = Request::new("GET", "/path%20space", &headers, &[]);
assert_eq!(req.path, "/path%20space");
let req = Request::new("GET", "/path+plus", &headers, &[]);
assert_eq!(req.path, "/path+plus");
let req = Request::new("GET", "/path/to/somewhere", &headers, &[]);
assert_eq!(req.path, "/path/to/somewhere");
}
#[test]
fn test_request_url() {
let headers = [];
let req = Request::new("GET", "/path?query=1", &headers, &[]);
assert_eq!(req.url().next(), Some(Cow::Borrowed("/path")));
let req = Request::new("GET", "/path", &headers, &[]);
assert_eq!(req.url().next(), Some(Cow::Borrowed("/path")));
let req = Request::new("GET", "/path%20space", &headers, &[]);
assert_eq!(req.url().next(), Some(Cow::Borrowed("/path space")));
let req = Request::new("GET", "/path+plus", &headers, &[]);
assert_eq!(req.url().next(), Some(Cow::Borrowed("/path+plus")));
let req = Request::new("GET", "/path/to/somewhere", &headers, &[]);
let mut url = req.url();
assert_eq!(url.next(), Some(Cow::Borrowed("/path")));
assert_eq!(url.next(), Some(Cow::Borrowed("/to")));
assert_eq!(url.next(), Some(Cow::Borrowed("/somewhere")));
let req = Request::new("GET", "path/to/somewhere", &headers, &[]);
let mut url = req.url();
assert_eq!(url.next(), Some(Cow::Borrowed("path")));
assert_eq!(url.next(), Some(Cow::Borrowed("/to")));
assert_eq!(url.next(), Some(Cow::Borrowed("/somewhere")));
}
#[test]
fn test_response_constructors() {
let r = Response::ok();
assert_eq!(r.status, 200);
let r = Response::not_found();
assert_eq!(r.status, 404);
let r = Response::bad_request();
assert_eq!(r.status, 400);
let r = Response::temporary_redirect("/foo");
assert_eq!(r.status, 307);
assert_eq!(r.headers[0].0, "Location");
assert_eq!(r.headers[0].1, "/foo");
}
#[test]
fn test_response_with_body() {
let r = Response::ok().with_body("hello", Some("text/plain"));
assert_eq!(r.status, 200);
assert!(r.body.is_some());
assert_eq!(
r.headers
.iter()
.find(|&(n, _)| n == "Content-Type")
.unwrap()
.1,
"text/plain"
);
let r = r.with_body("world", Some("application/json"));
assert_eq!(
r.headers
.iter()
.find(|&(n, _)| n == "Content-Type")
.unwrap()
.1,
"application/json"
);
assert_eq!(
r.headers
.iter()
.filter(|&(n, _)| n == "Content-Type")
.count(),
1
);
let r = r.with_body("data", Body::DEFAULT_CONTENT_TYPE);
assert!(!r.headers.iter().any(|(n, _)| n == "Content-Type"));
}
#[test]
fn test_request_params_plus_decoding() {
let headers = [];
let req = Request::new("GET", "/?a=b+c&d=e%20f&g=%2B", &headers, &[]);
let params = req.params();
assert_eq!(params.find("a").next(), Some(Cow::Borrowed("b c")));
assert_eq!(params.find("d").next(), Some(Cow::Owned("e f".to_string())));
assert_eq!(params.find("g").next(), Some(Cow::Owned("+".to_string())));
}
}