summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
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(())
+ }
+}