-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add README,
util::{cookies, datetime}
- Loading branch information
Showing
11 changed files
with
667 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
|
||
--> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ mod hash; | |
mod name; | ||
mod value; | ||
|
||
pub mod util; | ||
|
||
pub use name::{Header, standard}; | ||
pub use value::Value; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.