summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cargo/config.toml2
-rw-r--r--Cargo.lock252
-rw-r--r--Cargo.toml5
-rw-r--r--config.lua13
-rw-r--r--src/client.rs88
-rw-r--r--src/handlers/mod.rs38
-rw-r--r--src/handlers/staticfile.rs83
-rw-r--r--src/main.rs189
-rw-r--r--src/request.rs55
-rw-r--r--src/response.rs52
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
diff --git a/Cargo.lock b/Cargo.lock
index 0330474..0843c9d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 2d752cd..9a77cad 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(())
+ }
+}