diff options
| -rw-r--r-- | .cargo/config.toml | 2 | ||||
| -rw-r--r-- | Cargo.lock | 252 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | config.lua | 13 | ||||
| -rw-r--r-- | src/client.rs | 88 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 38 | ||||
| -rw-r--r-- | src/handlers/staticfile.rs | 83 | ||||
| -rw-r--r-- | src/main.rs | 189 | ||||
| -rw-r--r-- | src/request.rs | 55 | ||||
| -rw-r--r-- | src/response.rs | 52 |
10 files changed, 693 insertions, 84 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..16f1e73 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"]
\ No newline at end of file @@ -3,21 +3,15 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.1.4" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "memchr", + "libc", ] [[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -40,6 +34,12 @@ dependencies = [ ] [[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -62,6 +62,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -127,21 +146,43 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" name = "httpd" version = "0.1.0" dependencies = [ - "anyhow", + "chrono", "http", "httparse", + "httpdate", "mlua", "thiserror", "tokio", ] [[package]] -name = "itertools" -version = "0.14.0" +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "either", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] @@ -151,6 +192,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -166,6 +217,12 @@ dependencies = [ ] [[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -193,7 +250,6 @@ dependencies = [ "futures-util", "libc", "mlua-sys", - "mlua_derive", "num-traits", "parking_lot", "rustc-hash", @@ -213,21 +269,6 @@ dependencies = [ ] [[package]] -name = "mlua_derive" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465bddde514c4eb3b50b543250e97c1d4b284fa3ef7dc0ba2992c77545dbceb2" -dependencies = [ - "itertools", - "once_cell", - "proc-macro-error2", - "proc-macro2", - "quote", - "regex", - "syn", -] - -[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -278,28 +319,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -327,35 +346,6 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -512,12 +502,110 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,7 +6,8 @@ edition = "2024" [dependencies] http = "1.4.0" httparse = "1.10.1" +httpdate = "1.0.3" thiserror = "2.0.18" -anyhow = "1.0.102" -mlua = { version = "0.11.6", features = ["luajit", "async", "send", "macros"]} +chrono = "0.4.44" +mlua = { version = "0.11.6", features = ["luajit", "async", "error-send"] } tokio = { version = "1.49.0", features = ["full"] }
\ No newline at end of file diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..6ca8ff5 --- /dev/null +++ b/config.lua @@ -0,0 +1,13 @@ +http = {} + +http.bind = "localhost:8080" + +http.handlers = {} + +http.handlers["GET"] = function(request) + return { + handler = "staticfile", + path = "/tmp/foo.txt", + mime = "text/plain" + } +end diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..c5c8e18 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,88 @@ +use httparse::EMPTY_HEADER; + +use tokio::io::{self, AsyncBufRead, AsyncBufReadExt, AsyncWrite, AsyncWriteExt}; + +use crate::{request::Request, response::Response}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("unsupported version")] + Version, + + #[error("io error: {0}")] + Io(#[from] io::Error), + + #[error("invalid method: {0}")] + Method(#[from] http::method::InvalidMethod), + + #[error("http error: {0}")] + Http(#[from] http::Error), + + #[error("http parse error: {0}")] + Parse(#[from] httparse::Error), +} + +pub struct Client<R, W> { + reader: R, + writer: W, +} + +impl<R, W> Client<R, W> +where + R: AsyncBufRead + Unpin, + W: AsyncWrite + Unpin, +{ + pub fn new(reader: R, writer: W) -> Self { + Self { reader, writer } + } + + pub async fn send_response(&mut self, response: Response) -> io::Result<()> { + response.to_wire(&mut self.writer).await?; + + self.writer.flush().await?; + + Ok(()) + } + + pub async fn read_request(&mut self) -> Result<Option<Request<Vec<u8>>>, Error> { + let mut buf = Vec::new(); + let mut line = Vec::new(); + + loop { + line.clear(); + + if self.reader.read_until(b'\n', &mut line).await? == 0 { + return Ok(None); + } + + if line == b"\r\n" || line.is_empty() { + break; + } + + buf.extend_from_slice(&line); + buf.extend_from_slice(b"\r\n"); + } + + let mut headers = [EMPTY_HEADER; 64]; + let mut parsed = httparse::Request::new(&mut headers); + + parsed.parse(&buf)?; + + let mut builder = http::Request::builder(); + + builder = builder.method(http::Method::from_bytes(parsed.method.unwrap().as_bytes())?); + builder = builder.uri(parsed.path.unwrap()); + builder = builder.version(match parsed.version.unwrap() { + 1 => http::Version::HTTP_11, + _ => return Err(Error::Version), + }); + + for header in parsed.headers { + builder = builder.header(header.name, header.value); + } + + let body: Vec<u8> = Vec::new(); + + Ok(Some(Request::new(builder.body(body)?))) + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..800b61e --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,38 @@ +use mlua::{FromLua, Value}; + +use crate::{handlers::staticfile::StaticFile, request::Request, response::Response}; + +mod staticfile; + +pub(super) trait Handle<T> { + async fn handle(&self, request: Request<T>) -> Result<Response, Error>; +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("unsupported method")] + Unsupported, + + #[error("static file handler error: {0}")] + StaticFile(#[from] staticfile::Error), +} + +#[derive(Debug, Clone)] +pub enum Handlers { + StaticFile(StaticFile), +} + +impl FromLua for Handlers { + fn from_lua(value: Value, lua: &mlua::Lua) -> mlua::Result<Self> { + match value { + Value::Table(table) => match table.get::<String>("handler")?.as_str() { + "staticfile" => Ok(Self::StaticFile(StaticFile::from_lua( + Value::Table(table.clone()), + lua, + )?)), + _ => Err(mlua::Error::runtime("unknown handler")), + }, + _ => Err(mlua::Error::runtime("expected table")), + } + } +} diff --git a/src/handlers/staticfile.rs b/src/handlers/staticfile.rs new file mode 100644 index 0000000..a765315 --- /dev/null +++ b/src/handlers/staticfile.rs @@ -0,0 +1,83 @@ +use std::{ffi::OsString, os::unix::ffi::OsStringExt, path::PathBuf, time}; + +use mlua::{FromLua, Value}; +use tokio::{ + fs::{self, File}, + io, +}; + +use crate::{ + Handle, handlers, + request::Request, + response::{self, Response}, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("io error: {0}")] + Io(#[from] io::Error), + + #[error("http error: {0}")] + Http(#[from] http::Error), +} + +#[derive(Debug, Clone)] +pub struct StaticFile { + path: PathBuf, + mime: String, +} + +impl<T> Handle<T> for StaticFile { + async fn handle(&self, request: Request<T>) -> Result<Response, handlers::Error> { + if let http::Method::GET | http::Method::HEAD = request.inner().method().clone() { + if !fs::try_exists(&self.path).await.map_err(Error::Io)? { + return Ok(Response::new( + http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body(response::Body::Empty) + .map_err(Error::Http)?, + )); + } + + let file = File::open(&self.path).await.map_err(Error::Io)?; + let metadata = file.metadata().await.map_err(Error::Io)?; + + let now = time::SystemTime::now(); + let date = httpdate::fmt_http_date(now); + + let response = http::Response::builder() + .status(http::StatusCode::OK) + .header("CONTENT-LENGTH", metadata.len()) + .header("CONTENT-TYPE", &self.mime) + .header("DATE", date); + + match request.inner().method().clone() { + http::Method::GET => Ok(Response::new( + response + .body(response::Body::File(file)) + .map_err(Error::Http)?, + )), + http::Method::HEAD => Ok(Response::new( + response.body(response::Body::Empty).map_err(Error::Http)?, + )), + _ => unreachable!(), + } + } else { + Err(handlers::Error::Unsupported) + } + } +} + +impl FromLua for StaticFile { + fn from_lua(value: mlua::Value, _: &mlua::Lua) -> mlua::Result<Self> { + match value { + Value::Table(table) => Ok(Self { + path: PathBuf::from(OsString::from_vec( + table.get::<mlua::String>("path")?.as_bytes().to_vec(), + )), + mime: table.get::<String>("mime")?, + }), + _ => Err(mlua::Error::runtime("expected table")), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ea459d9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,189 @@ +#![allow(dead_code)] + +use std::process::ExitCode; + +use mlua::{Function, Lua, Table}; + +use tokio::{ + io::{self, BufReader, BufWriter}, + net::{TcpListener, TcpStream}, +}; + +use crate::{ + client::Client, + handlers::{Handle, Handlers}, + request::Request, + response::Response, +}; + +mod client; +mod handlers; +mod request; +mod response; + +macro_rules! exit { + ($fmt:literal, $($s:expr),*) => { + { + eprintln!($fmt, $($s),*); + + return ::std::process::ExitCode::FAILURE + } + } +} + +#[derive(Debug, thiserror::Error)] +enum HandleError { + #[error("error reading handler function: {0}")] + Function(mlua::Error), + + #[error("error calling handler: {0}")] + InvokeHandler(mlua::Error), + + #[error("handler error: {0}")] + Handler(#[from] handlers::Error), +} + +#[derive(Debug, thiserror::Error)] +enum ResponseError { + #[error("error reading request: {0}")] + Request(client::Error), + + #[error("error sending response: {0}")] + Response(io::Error), +} + +#[derive(Debug, thiserror::Error)] +enum InitLuaError { + #[error("failed to create variable: {0}")] + CreateTable(mlua::Error), + + #[error("failed to set variable {1}: {0}")] + SetVar(mlua::Error, String), + + #[error("failed to load variable {1}: {0}")] + LoadVar(mlua::Error, String), + + #[error("failed to load config: {0}")] + LoadConfig(std::io::Error), + + #[error("failed to eval config: {0}")] + EvalConfig(mlua::Error), +} + +async fn handle<T: Clone + 'static>( + handlers: Table, + request: Request<T>, +) -> Result<Response, HandleError> { + let method = request.inner().method().as_str().to_string(); + + let function = handlers + .get::<Function>(method.as_str()) + .map_err(HandleError::Function)?; + + let handler = function + .call::<Handlers>(request.clone()) + .map_err(HandleError::InvokeHandler)?; + + match handler { + Handlers::StaticFile(staticfile) => Ok(staticfile.handle(request).await?), + } +} + +async fn response(handlers: Table, stream: TcpStream) -> Result<(), ResponseError> { + let mut client = { + let (r, w) = stream.into_split(); + + Client::new(BufReader::new(r), BufWriter::new(w)) + }; + + while let Some(request) = client + .read_request() + .await + .map_err(ResponseError::Request)? + { + let response = match handle(handlers.clone(), request).await { + Ok(response) => response, + Err(e) => { + eprintln!("failed to handle request: {e:?}"); + + Response::new( + http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(response::Body::Empty) + .unwrap(), + ) + } + }; + + client + .send_response(response) + .await + .map_err(ResponseError::Response)?; + } + + Ok(()) +} + +fn init_lua(lua: Lua) -> Result<(), InitLuaError> { + let http = lua.create_table().map_err(InitLuaError::CreateTable)?; + + lua.globals() + .set("http", http.clone()) + .map_err(|e| InitLuaError::SetVar(e, "http".to_string()))?; + + let chunk = lua.load(std::fs::read_to_string("config.lua").map_err(InitLuaError::LoadConfig)?); + + chunk.eval::<()>().map_err(InitLuaError::EvalConfig)?; + + Ok(()) +} + +#[allow(unexpected_cfgs)] +#[tokio::main(flavor = "local")] +async fn main() -> ExitCode { + let lua = Lua::new(); + + if let Err(e) = init_lua(lua.clone()) { + exit!("failed to init lua: {}", e); + } + + let http = match lua.globals().get::<Table>("http") { + Ok(http) => http, + Err(e) => exit!("failed to load table 'http': {}", e), + }; + + let bind = match http.get::<String>("bind") { + Ok(bind) => bind, + Err(e) => exit!("failed to load string 'http.bind': {}", e), + }; + + let handlers = match http.get::<Table>("handlers") { + Ok(handlers) => handlers, + Err(e) => exit!("failed to load 'http.handlers': {}", e), + }; + + let listener = match TcpListener::bind(&bind).await { + Ok(listener) => listener, + Err(e) => exit!("failed to bind to {}: {}", bind, e), + }; + + loop { + let (stream, addr) = match listener.accept().await { + Ok((stream, addr)) => (stream, addr), + Err(e) => { + eprintln!("failed to accept connection: {e}"); + continue; + } + }; + + eprintln!("accepted connection from {addr}"); + + let future = response(handlers.clone(), stream); + + tokio::task::spawn_local(async { + if let Err(e) = future.await { + eprintln!("response failure: {e:?}"); + } + }); + } +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..2814a14 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,55 @@ + +use mlua::UserData; + +use tokio::io::{self}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("error parsing request: {0}")] + Parse(#[from] httparse::Error), + + #[error("io error: {0}")] + Io(#[from] io::Error), + + #[error("invalid method: {0}")] + Method(#[from] http::method::InvalidMethod), + + #[error("invalid request: {0}")] + Request(#[from] http::Error), + + #[error("unsupported version")] + Version, +} + +#[derive(Debug, Clone)] +pub struct Request<T>(http::Request<T>); + +impl<T> Request<T> { + pub fn inner(&self) -> &http::Request<T> { + &self.0 + } + + pub fn new(request: http::Request<T>) -> Self { + Self(request) + } +} + +impl<T> UserData for Request<T> { + fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) { + fields.add_field_method_get("method", |_, this| { + Ok(this.inner().method().as_str().to_string()) + }); + + fields.add_field_method_get("path", |_, this| Ok(this.inner().uri().path().to_string())); + + fields.add_field_method_get("headers", |lua, this| { + let table = lua.create_table()?; + + for (key, value) in this.inner().headers() { + table.set(key.as_str(), value.as_bytes())?; + } + + Ok(table) + }) + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..801e682 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,52 @@ +use tokio::{ + fs::File, + io::{self, AsyncWrite, AsyncWriteExt}, +}; + +#[derive(Debug)] +pub enum Body { + File(File), + Bytes(Vec<u8>), + Empty, +} + +#[derive(Debug)] +pub struct Response(http::Response<Body>); + +impl Response { + pub fn new(inner: http::Response<Body>) -> Self { + Self(inner) + } + + pub fn inner(&self) -> &http::Response<Body> { + &self.0 + } +} + +impl Response { + pub async fn to_wire<W: AsyncWrite + Unpin>(self, writer: &mut W) -> io::Result<()> { + writer + .write_all(format!("HTTP/1.1 {}\r\n", self.0.status()).as_bytes()) + .await?; + + for (key, value) in self.0.headers() { + writer.write_all(format!("{key}: ").as_bytes()).await?; + writer.write_all(value.as_bytes()).await?; + writer.write_all(b"\r\n").await?; + } + + writer.write_all(b"\r\n").await?; + + match self.0.into_body() { + Body::File(mut file) => { + io::copy(&mut file, writer).await?; + } + Body::Bytes(buf) => { + writer.write_all(&buf).await?; + } + Body::Empty => (), + } + + Ok(()) + } +} |
