diff options
Diffstat (limited to 'src')
| -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 |
6 files changed, 505 insertions, 0 deletions
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(()) + } +} |
