From 965aa855ee341c0c478fa476bdeb30ff16b456ab Mon Sep 17 00:00:00 2001 From: kana-rus Date: Thu, 24 Oct 2024 22:27:18 +0900 Subject: [PATCH 1/2] add `example` & update READNE and some for that --- .gitignore | 4 ++-- LICENSE | 25 ++++++++++++++++++++ README.md | 55 ++++++++++++++++++++++++++++++++++++++++---- Taskfile.yaml | 2 +- example/Cargo.toml | 8 +++++++ example/src/main.rs | 52 +++++++++++++++++++++++++++++++++++++++++ src/http1/load.rs | 45 ++++++++++++++++++++---------------- src/http1/mod.rs | 2 +- src/http1/send.rs | 10 ++++++-- src/lib.rs | 2 ++ src/request/mod.rs | 19 ++++++++++++--- src/response/body.rs | 6 ++--- src/response/mod.rs | 2 +- 13 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 LICENSE create mode 100644 example/Cargo.toml create mode 100644 example/src/main.rs diff --git a/.gitignore b/.gitignore index 2ebc5ea..2a6fee4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/target -/Cargo.lock \ No newline at end of file +**/target +**/Cargo.lock \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b100387 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2024 kanarus + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 9ac3628..d7401ec 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,11 @@
- License + License CI status crates.io
-
- ## What's advantage over http crate? ### fast, efficient @@ -21,6 +19,7 @@ * 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* +* [micro benchmarks](https://github.com/ohkami-rs/whttp/blob/main/benches) ### batteries included @@ -31,8 +30,54 @@ * HTTP/1.1 parsing & writing on `http1` & `rt_*` feature * supported runtimes ( `rt_*` ) : `tokio`, `async-std`, `smol`, `glommio` -
+## [Example](https://github.com/ohkami-rs/whttp/blob/main/example) + +```toml +[dependencies] +whttp = { version = "0.1", features = ["http1", "rt_tokio"] } +tokio = { version = "1", features = ["full"] } +``` +```rust +use whttp::{Request, Response, http1}; +use whttp::header::{ContentType, Date}; +use whttp::util::IMFfixdate; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let listener = tokio::net::TcpListener::bind("localhost:3000").await?; + + while let Ok((mut conn, addr)) = listener.accept().await { + let mut req = http1::init(); + let mut req = std::pin::Pin::new(&mut req); + + while let Ok(Some(())) = http1::load( + req.as_mut(), &mut conn + ).await { + let res = handle(&req).await; + http1::send(res, &mut conn).await?; + } + } + + Ok(()) +} + +async fn handle(req: &Request) -> Response { + if !(req.header(ContentType) + .is_some_and(|ct| ct.starts_with("text/plain")) + ) { + return Response::BadRequest() + .with(Date, IMFfixdate::now()) + .with_text("expected text payload") + } + + let name = std::str::from_utf8(req.body().unwrap()).unwrap(); -## Example + Response::OK() + .with(Date, IMFfixdate::now()) + .with_text(format!("Hello, {name}!")) +} +``` +## LICENSE +whttp is licensed under MIT LICENSE ( [LICENSE](https://github.com/ohkami-rs/whttp/blob/main/LICENSE) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) ). diff --git a/Taskfile.yaml b/Taskfile.yaml index b11b725..bcb25e1 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -13,7 +13,7 @@ tasks: test:doc: cmds: - - cargo test --doc --features sse,ws,http1,rt_tokio + - cargo test --doc --features DEBUG test:default: cmds: diff --git a/example/Cargo.toml b/example/Cargo.toml new file mode 100644 index 0000000..cc72d40 --- /dev/null +++ b/example/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.1.0" +edition = "2021" + +[dependencies] +whttp = { path = "..", features = ["http1", "rt_tokio"] } +tokio = { version = "1", features = ["full"] } diff --git a/example/src/main.rs b/example/src/main.rs new file mode 100644 index 0000000..6d5e367 --- /dev/null +++ b/example/src/main.rs @@ -0,0 +1,52 @@ +use whttp::{Request, Response, http1}; +use whttp::header::{ContentType, Date}; +use whttp::util::IMFfixdate; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let listener = tokio::net::TcpListener::bind("localhost:3000").await?; + + println!("Started serving at localhost:3000"); + println!("Try\n\ + $ curl -v http://localhost:3000\n\ + $ curl -v http://localhost:3000 http://localhost:3000\n\ + $ curl -v http://localhost:3000 -H 'Content-Type: text/plain' -d '{{YOUR NAME}}'\n\ + "); + + while let Ok((mut conn, addr)) = listener.accept().await { + println!("accepcted {addr}\n"); + + let mut req = http1::init(); + let mut req = std::pin::Pin::new(&mut req); + + while let Ok(Some(())) = http1::load( + req.as_mut(), &mut conn + ).await { + println!("req = {req:?}"); + + let res = handle(&req).await; + println!("res = {res:?}"); + + http1::send(res, &mut conn).await?; + println!(); + } + } + + Ok(()) +} + +async fn handle(req: &Request) -> Response { + if !(req.header(ContentType) + .is_some_and(|ct| ct.starts_with("text/plain")) + ) { + return Response::BadRequest() + .with(Date, IMFfixdate::now()) + .with_text("expected text payload") + } + + let name = std::str::from_utf8(req.body().unwrap()).unwrap(); + + Response::OK() + .with(Date, IMFfixdate::now()) + .with_text(format!("Hello, {name}!")) +} diff --git a/src/http1/load.rs b/src/http1/load.rs index 3fbb966..c29340e 100644 --- a/src/http1/load.rs +++ b/src/http1/load.rs @@ -4,10 +4,15 @@ use std::{pin::Pin, io::ErrorKind, str::FromStr as _}; const PAYLOAD_LIMIT: usize = 1 << 32; +pub fn init() -> Request { + parse::new() +} + pub async fn load( mut req: Pin<&mut Request>, conn: &mut (impl Read + Unpin) ) -> Result, Status> { + parse::clear(&mut req); let buf = parse::buf(req.as_mut()); match conn.read(&mut **buf).await { @@ -108,56 +113,56 @@ async fn test_load_request() { let mut req = Pin::new(&mut req); { - let mut case: &[u8] = {parse::clear(&mut req); b"\ - "}; + let mut case: &[u8] = b"\ + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(None)); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ GET /HTTP/2\r\n\ \r\n\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Err(Status::BadRequest)); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ GET / HTTP/2\r\n\ \r\n\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Err(Status::HTTPVersionNotSupported)); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ GET / HTTP/1.1\r\n\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Err(Status::BadRequest)); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ GET / HTTP/1.1\r\n\ \r\n\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(()))); assert_eq!(*req, Request::GET("/")); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ GET / HTTP/1.1\r\n\ Host: http://127.0.0.1:3000\r\n\ \r\n\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(()))); assert_eq!(*req, Request::GET("/").with(Host, "http://127.0.0.1:3000")); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ POST /api/users HTTP/1.1\r\n\ Host: http://127.0.0.1:3000\r\n\ Content-Type: application/json\r\n\ Content-Length: 24\r\n\ \r\n\ {\"name\":\"whttp\",\"age\":0}\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(()))); assert_eq!(*req, Request::POST("/api/users") @@ -166,14 +171,14 @@ async fn test_load_request() { ); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ POST /api/users HTTP/1.1\r\n\ Host: http://127.0.0.1:3000\r\n\ Content-Type: application/json\r\n\ Content-Length: 22\r\n\ \r\n\ {\"name\":\"whttp\",\"age\":0}\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(()))); assert_eq!(*req, Request::POST("/api/users") @@ -182,14 +187,14 @@ async fn test_load_request() { ); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ POST /api/users HTTP/1.1\r\n\ host: http://127.0.0.1:3000\r\n\ content-type: application/json\r\n\ content-length: 24\r\n\ \r\n\ {\"name\":\"whttp\",\"age\":0}\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(()))); assert_eq!(*req, Request::POST("/api/users") @@ -198,14 +203,14 @@ async fn test_load_request() { ); } { - let mut case: &[u8] = {parse::clear(&mut req); b"\ + let mut case: &[u8] = b"\ POST /api/users HTTP/1.1\r\n\ host: http://127.0.0.1:3000\r\n\ content-type: application/json\r\n\ content-Length: 24\r\n\ \r\n\ {\"name\":\"whttp\",\"age\":0}\ - "}; + "; assert_eq!(load(req.as_mut(), &mut case).await, Ok(Some(()))); assert_eq!(*req, Request::POST("/api/users") diff --git a/src/http1/mod.rs b/src/http1/mod.rs index d676e6e..7aa5f58 100644 --- a/src/http1/mod.rs +++ b/src/http1/mod.rs @@ -1,5 +1,5 @@ mod load; -pub use load::load; +pub use load::{init, load}; mod send; pub use send::{send, Upgrade}; diff --git a/src/http1/send.rs b/src/http1/send.rs index a5944d4..b6b9968 100644 --- a/src/http1/send.rs +++ b/src/http1/send.rs @@ -1,5 +1,5 @@ -use crate::{Response, response::Body, io::Write}; -use crate::header::SetCookie; +use crate::{Response, Status, response::Body, io::Write}; +use crate::header::{ContentLength, SetCookie}; pub enum Upgrade { None, @@ -38,6 +38,12 @@ pub async fn send( mut res: Response, conn: &mut (impl Write + Unpin) ) -> Result { + if res.header(ContentLength).is_none() + && res.body().is_none() + && res.status() != Status::NoContent { + res.set(ContentLength, "0"); + } + let mut buf = [ b"HTTP/1.1 ", res.status().message().as_bytes(), b"\r\n" ].concat(); diff --git a/src/lib.rs b/src/lib.rs index e869212..de55025 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(feature="DEBUG", doc = include_str!("../README.md"))] + #[cfg(all( feature="ws", not(any( diff --git a/src/request/mod.rs b/src/request/mod.rs index e3f436c..b534a9a 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -246,12 +246,25 @@ pub mod parse { /// * `bytes` must be alive as long as `path` of `this` is in use; /// especially, reading from `this.buf` pub unsafe fn path(this: &mut Pin<&mut Request>, bytes: &[u8]) -> Result<(), Status> { - (bytes.len() > 0 && *bytes.get_unchecked(0) == b'/' && bytes.is_ascii()) - .then_some(this.path = Str::Ref(UnsafeRef::new( + if bytes.len() > 0 && bytes.is_ascii() { + #[cfg(debug_assertions)] + if *bytes.get_unchecked(0) != b'/' { + eprintln!("\ + Currently whttp only supports requests starting with \ + ` ` , NOT ` `, on expectation that \ + the host (and port) is contained in `Host` header. \ + "); + return Err(Status::NotImplemented) + } + + Ok(this.path = Str::Ref(UnsafeRef::new( // SAFETY: already checked `bytes` is ascii std::str::from_utf8_unchecked(bytes) ))) - .ok_or(Status::BadRequest) + + } else { + Err(Status::BadRequest) + } } #[inline] diff --git a/src/response/body.rs b/src/response/body.rs index ee7c660..c696258 100644 --- a/src/response/body.rs +++ b/src/response/body.rs @@ -36,17 +36,17 @@ impl PartialEq for Body { impl std::fmt::Debug for Body { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Payload(p) => f.debug_tuple("Body::Payload") + Self::Payload(p) => f.debug_tuple("Payload") .field(&p.escape_ascii().to_string()) .finish(), #[cfg(feature="sse")] - Self::Stream(_) => f.debug_tuple("Body::Stream") + Self::Stream(_) => f.debug_tuple("Stream") .field(&"...") .finish(), #[cfg(feature="ws")] - Self::WebSocket(_) => f.debug_tuple("Body::WebSocket") + Self::WebSocket(_) => f.debug_tuple("WebSocket") .field(&"...") .finish() } diff --git a/src/response/mod.rs b/src/response/mod.rs index 2b4eb22..4856d0e 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -182,7 +182,7 @@ impl Response { #[inline(always)] pub fn with(mut self, header: &Header, value: impl Into) -> Self { - self.headers.insert(header, value); + self.headers.append(header, value); self } From d96906ef7759a8220fbac956c05ee1ddf40983cc Mon Sep 17 00:00:00 2001 From: kana-rus Date: Thu, 24 Oct 2024 22:34:30 +0900 Subject: [PATCH 2/2] update features --- Cargo.toml | 6 ++++-- README.md | 2 +- Taskfile.yaml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 647fbaa..5bee3b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,10 @@ http1_smol = ["http1", "rt_smol"] http1_glommio = ["http1", "rt_glommio"] ### DEBUG ### -DEBUG = [] -### default = ["DEBUG", "sse", "ws", "http1", "rt_tokio", "tokio/full"] +DEBUG = [] +DEV = ["DEBUG", "sse", "ws", "http1", "rt_tokio"] +DOCTEST = ["DEV", "tokio?/full"] +### default = ["DEV"] [dev-dependencies] http = "1.1" diff --git a/README.md b/README.md index d7401ec..63466ae 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ whttp = { version = "0.1", features = ["http1", "rt_tokio"] } tokio = { version = "1", features = ["full"] } ``` -```rust +```rust,no_run use whttp::{Request, Response, http1}; use whttp::header::{ContentType, Date}; use whttp::util::IMFfixdate; diff --git a/Taskfile.yaml b/Taskfile.yaml index bcb25e1..7a0594c 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -13,7 +13,7 @@ tasks: test:doc: cmds: - - cargo test --doc --features DEBUG + - cargo test --doc --features DOCTEST test:default: cmds: