Skip to content

Commit

Permalink
add README, util::{cookies, datetime}
Browse files Browse the repository at this point in the history
  • Loading branch information
kanarus committed Oct 24, 2024
1 parent e0bf735 commit 547fe70
Show file tree
Hide file tree
Showing 11 changed files with 667 additions and 121 deletions.
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ homepage = "https://crates.io/crates/whttp"
repository = "https://github.com/ohkami-rs/whttp"
readme = "README.md"
license = "MIT"
description = "A new, opinionated implementation of HTTP types"
description = "A new, opinionated implementation of HTTP types for Rust"
keywords = ["http"]
categories = ["web-programming"]

Expand Down Expand Up @@ -50,8 +50,8 @@ http1_smol = ["http1", "rt_smol"]
http1_glommio = ["http1", "rt_glommio"]

### DEBUG ###
DEBUG = ["tokio?/full"]
default = ["DEBUG", "sse", "ws", "http1", "rt_tokio"]
DEBUG = []
### default = ["DEBUG", "sse", "ws", "http1", "rt_tokio", "tokio/full"]

[dev-dependencies]
http = "1.1"
Expand Down
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
<!--
<div align="center">
<h1>whttp</h1>
A new, opinionated implementation of HTTP types
A new, opinionated implementation of HTTP types for Rust
</div>

<br>

<div align="right">
<a href="https://github.com/ohkami-rs/whttp/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/crates/l/ohkami.svg" /></a>
<a href="https://github.com/ohkami-rs/whttp/actions"><img alt="CI status" src="https://github.com/ohkami-rs/whttp/actions/workflows/CI.yml/badge.svg"/></a>
<a href="https://crates.io/crates/whttp"><img alt="crates.io" src="https://img.shields.io/crates/v/whttp" /></a>
</div>

<br>

* _efficient_
* minimum memory copy & allocation in request parsing
* pre-calculated fxhash for headers
## What's advantage over http crate?

### fast, efficient

* swiss table (by hashbrown) and pre-calculated fxhash for `Headers`
* pre-matching standard headers before hashing in parsing
* `Request` construction with zero or least copy from parsing buffer and very minimum allocation
* size of `Request` is *128* and size of `Response` is *64*

### batteries included

* consistent and clear API
* builtin support for Cookie, Set-Cookie, IMF-fixdate header values and JSON response body
* Server-Sent Events on `sse` feature
* WebSocket on `ws` & `rt_*` feature
* HTTP/1.1 parsing & writing on `http1` & `rt_*` feature
* supported runtimes ( `rt_*` ) : `tokio`, `async-std`, `smol`, `glommio`

<br>

## Example

* _battery included_
* Cookie / Set-Cookie support
* Server-Sent Events : `sse` feature
* WebSocket : `ws` with `rt_*` feature
* HTTP/1.1 parsing & writing : `http1` with `rt_*` feature
* supported runtimes ( `rt_*` ) : `tokio`, `async-std`, `smol`, `glommio`

-->
4 changes: 2 additions & 2 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ version: 3
tasks:
CI:
deps:
- test:doc
- test:default
- task: test:doc
- task: test:default
- for: [tokio, async-std, smol, glommio]
task: test:rt
vars: { rt: '{{.ITEM}}' }
Expand Down
2 changes: 2 additions & 0 deletions src/headers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ mod hash;
mod name;
mod value;

pub mod util;

pub use name::{Header, standard};
pub use value::Value;

Expand Down
5 changes: 5 additions & 0 deletions src/headers/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod datetime;
pub use datetime::IMFfixdate;

mod cookies;
pub use cookies::{cookie, setcookie};
208 changes: 208 additions & 0 deletions src/headers/util/cookies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use crate::{headers::Value, util::IntoStr};
use std::borrow::Cow;
use percent_encoding::{utf8_percent_encode, percent_decode_str, NON_ALPHANUMERIC};

/*=====================================================*/

/// `Cookie` helper
///
/// *example.rs*
/// ```
/// # use whttp::{Request, Response, util::cookie};
/// # use whttp::header::SetCookie;
/// #
/// fn get_cookies(req: &Request) ->
/// Option<impl Iterator<Item = cookie<'_>>>
/// {
/// req.cookies()
/// }
///
/// fn add_cookie(res: &mut Response) {
/// res.append(SetCookie,
/// cookie::set("token", "abcxyz")
/// .Secure()
/// .HttpOnly()
/// );
/// }
/// ```
#[allow(non_camel_case_types)]
pub struct cookie<'req> {
name: &'req str,
value: &'req str
}

impl<'req> cookie<'req> {
pub(crate) fn parse(cookies: &'req str) -> impl Iterator<Item = Self> {
cookies.split("; ").flat_map(|cookie|
cookie.split_once('=').map(|(name, value)|
cookie { name, value }))
}
}

impl<'req> cookie<'req> {
pub fn name(&self) -> &str {
self.name
}

pub fn value(&self) -> Cow<'_, str> {
percent_decode_str(self.value).decode_utf8()
.map_err(|_| self.value).expect("non UTF-8 Cookie value")
}
pub fn value_bytes(&self) -> Cow<'_, [u8]> {
percent_decode_str(self.value).into()
}
}

/*=====================================================*/

impl cookie<'static> {
pub fn set(name: &str, value: &str) -> setcookie {
setcookie::new(name, value)
}

pub fn set_encoded(name: &str, value: &str) -> setcookie {
setcookie::encoded(name, value)
}
}

/// `Set-Cookie` helper
///
/// *example.rs*
/// ```
/// # use whttp::{Headers, util::cookie};
/// # use whttp::header::SetCookie;
/// #
/// fn add_cookie(headers: &mut Headers) {
/// headers
/// .append(SetCookie,
/// cookie::set("token", "abcxyz")
/// .Secure()
/// .HttpOnly()
/// );
/// }
/// ```
#[allow(non_camel_case_types)]
pub struct setcookie(String);

const _: () = {
impl Into<Value> for setcookie {
#[inline]
fn into(self) -> Value {
Value::from(self.0)
}
}

impl std::ops::Deref for setcookie {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
};

fn valid_name(name: &str) -> bool {
name.bytes().all(|b| matches!(b,
// 0 ..=31 are controls
// 32 is ' '
| 33 // 34 is '"'
| 35..=39 // 40..=41 are '(' ')'
| 42..=43 // 44 is ','
| 45..=46 // 47 is '/'
| 48..=57 // 58..=64 are ':' ';' '<' '=' '>' '?' '@'
| 65..=90 // 91..=93 are '[' '\' ']'
| 94..=122 // 123 is '{'
| 124 // 125 is '}'
| 126 // 127 is DEL (control)
))
}
fn valid_value(value: &str) -> bool {
value.bytes().all(|b| b.is_ascii() && !!!(
b.is_ascii_control() ||
b.is_ascii_whitespace() ||
matches!(b, b'"' | b',' | b';' | b'\\')
))
}
fn quoted_content(value: &str) -> Option<&str> {
if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') {
Some(&value[1..value.len()-1])
} else {
None
}
}

impl setcookie {
pub fn new(name: &str, value: &str) -> Self {
assert!(valid_name(name), "\
`{name}` can't be a Set-Cookie name: it must be ascii and not be controls, spaces or separators\
(https://httpwg.org/specs/rfc6265.html#sane-set-setcookie-syntax) \
");

let (value, quoted) = match quoted_content(value) {
Some(q) => (q, true),
None => (value, false)
};
assert!(valid_value(value), "\
`{value}` can't be a Set-Cookie value: it must be ascii and not be controls, whitespaces or `\"` `,` `;` `\\` \
(https://httpwg.org/specs/rfc6265.html#sane-set-setcookie-syntax) \
");

Self(if quoted {
[name, "=\"", value, "\""].concat()
} else {
[name, "=", value].concat()
})
}

pub fn encoded(name: &str, value: &str) -> Self {
let value = utf8_percent_encode(
quoted_content(value).unwrap_or(value),
NON_ALPHANUMERIC
);
Self::new(name, &Cow::from(value))
}
}

#[allow(non_snake_case)]
impl setcookie {
pub fn Expires(mut self, Expires: super::IMFfixdate) -> Self {
self.0.push_str("; Expires=");
self.0.push_str(&Expires);
self
}
pub fn MaxAge(mut self, MaxAge: u64) -> Self {
self.0.push_str("; Max-Age=");
self.0.push_str(&MaxAge.to_string());
self
}
pub fn Domain(mut self, Domain: impl IntoStr) -> Self {
self.0.push_str("; Domain=");
self.0.push_str(&Domain.into_str());
self
}
pub fn Path(mut self, Path: impl IntoStr) -> Self {
self.0.push_str("; Path=");
self.0.push_str(&Path.into_str());
self
}
pub fn Secure(mut self) -> Self {
self.0.push_str("; Secure");
self
}
pub fn HttpOnly(mut self) -> Self {
self.0.push_str("; HttpOnly");
self
}
pub fn SameSiteStrict(mut self) -> Self {
self.0.push_str("; SameSite=Strict");
self
}
pub fn SameSiteLax(mut self) -> Self {
self.0.push_str("; SameSite=Lax");
self
}
pub fn SameSiteNone(mut self) -> Self {
self.0.push_str("; SameSite=None");
self
}
}
Loading

0 comments on commit 547fe70

Please sign in to comment.