Files
webserver/src/main.rs
2025-05-04 22:12:49 +02:00

490 lines
16 KiB
Rust

mod icon;
mod index;
// mod lua;
use std::{collections::HashMap, fs, io::ErrorKind, path::PathBuf, str::FromStr, sync::LazyLock};
use index::render_markdown;
use mlua::{
Lua, Number, Table,
Value::{self as LuaValue},
};
use axum::{
extract::Request,
handler::HandlerWithoutStateExt,
http::{HeaderName, HeaderValue, StatusCode},
response::{Html, IntoResponse},
routing::get,
};
use serde::Serialize;
use time::OffsetDateTime;
use tokio::time::Instant;
use tower_http::services::ServeDir;
pub type AError = Box<dyn std::error::Error>;
pub type AResult<T> = Result<T, AError>;
pub static CWD: LazyLock<PathBuf> = LazyLock::new(|| std::env::current_dir().unwrap());
pub static LUA_CPATH: LazyLock<String> = LazyLock::new(|| {
String::from_utf8_lossy(
&std::process::Command::new("luarocks")
.arg("path")
.arg("--lr-cpath")
.arg("--full")
.output()
.map_err::<std::vec::Vec<u8>, _>(|_| vec![])
.map(|r| r.stdout)
.unwrap_or_default(),
)
.trim()
.to_string()
});
pub static LUA_PATH: LazyLock<String> = LazyLock::new(|| {
String::from_utf8_lossy(
&std::process::Command::new("luarocks")
.arg("path")
.arg("--lr-path")
.arg("--full")
.output()
.map_err::<std::vec::Vec<u8>, _>(|_| vec![])
.map(|r| r.stdout)
.unwrap_or_default(),
)
.trim()
.to_string()
});
pub static BASE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from_str(&std::env::var("BASE_DIR").unwrap_or(String::from("./public"))).unwrap()
});
#[derive(Debug, Serialize)]
struct FileInfo {
filename: String,
size: usize,
format: String,
icon: Option<Vec<u8>>,
modified: OffsetDateTime,
description: Option<String>,
}
#[tokio::main]
async fn main() -> AResult<()> {
let listener = tokio::net::TcpListener::bind("[::]:3000").await.unwrap();
println!("Server running on port 3000");
Ok(axum::serve(listener, file_handler.into_make_service()).await?)
}
async fn file_handler(request: Request) -> impl IntoResponse {
std::env::set_current_dir(&*CWD).unwrap();
let base_dir = BASE_DIR.canonicalize().map_err(|e| {
eprintln!("{:#?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
let mut request_uri_path = request.uri().path().to_owned();
if request_uri_path.ends_with("/") {
let mut a = base_dir.clone();
a.push(format!(".{}/{}", &request_uri_path, "init.lua"));
if let Ok(b) = a.canonicalize() {
if b.exists() {
request_uri_path += "init.lua";
}
}
}
if request_uri_path.to_lowercase().ends_with(".md") {
let mut path = base_dir.clone();
let uri = urlencoding::decode(&request_uri_path[1..]).map_err(|e| {
println!("{:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
path.push(&*uri);
let full_path = fs::canonicalize(&path).map_err(|e| {
println!("{:?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
let full_base_path = fs::canonicalize(&base_dir).map_err(|e| {
println!("{:?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
if !full_path.starts_with(&full_base_path) {
return Err((StatusCode::BAD_REQUEST, "400: Bad Request".to_string()));
}
let script = fs::read_to_string(&full_path).map_err(|e| {
eprintln!("Lua Read Error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
let filename = full_path.file_name().unwrap_or_default().to_string_lossy();
if let Ok(md) = markdown::to_html_with_options(&script, &markdown::Options::gfm()) {
return Ok(Html(render_markdown(&filename, &md).unwrap()).into_response());
}
}
if !request_uri_path.ends_with(".lua") {
return ServeDir::new(&base_dir)
.fallback(get(handler))
.try_call(request)
.await
.map(|e| e.into_response())
.map_err(|_| (StatusCode::NOT_FOUND, "404: Not Found".to_string()));
};
let lua = unsafe { Lua::unsafe_new() };
// Fix load paths
lua.load(format!(
"package.path = \"{}\"\npackage.cpath = \"{}\"",
&*LUA_PATH, &*LUA_CPATH
))
.exec()
.map_err(|e| {
eprintln!("Lua Error: {:#?}", e);
render_lua_error(e)
})?;
let globals = lua.globals();
let request_table = lua.create_table().map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
let headers_table = lua
.create_table_from(
request
.headers()
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap())),
)
.map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
let query = request.uri().query().unwrap_or("");
let query: HashMap<String, String> = serde_qs::from_str(query)
.map_err(|e| {
println!("{:#?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
"500: Unable to parse query string".to_string(),
)
})
.unwrap_or(HashMap::new());
let query_table = lua.create_table_from(query).map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
request_table.set("headers", headers_table).map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
request_table
.set("uri", request.uri().to_string())
.map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
request_table
.set("method", request.method().to_string())
.map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
request_table.set("query", query_table).map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
// Inject functions to change headers and such
globals.set("request", request_table).map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
let response_table = lua.create_table().map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
response_table
.set(
"headers",
lua.create_table().map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?,
)
.map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
globals.set("response", response_table).map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
let mut path = base_dir.clone();
let uri = urlencoding::decode(&request_uri_path[1..]).map_err(|e| {
println!("{:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
path.push(&*uri);
let full_path = fs::canonicalize(&path).map_err(|e| {
println!("{:?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
let full_base_path = fs::canonicalize(base_dir).map_err(|e| {
println!("{:?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
if !full_path.starts_with(&full_base_path) {
return Err((StatusCode::BAD_REQUEST, "400: Bad Request".to_string()));
}
let script = fs::read_to_string(full_path).map_err(|e| {
eprintln!("Lua Read Error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
// ------------------------------------------------------------------------
// TODO: FIX THIS, THIS IS DISGUSTING
std::env::set_current_dir(full_base_path).unwrap();
// Before executing user-code, make sure they can't screw with `global`
lua.load(
r#"
setmetatable(_G, {
__newindex = function (_, n)
error("attempt to write to undeclared variable "..n, 2)
end,
__index = function (_, n)
error("attempt to read undeclared variable "..n, 2)
end,
})"#,
)
.exec()
.map_err(|e| {
std::env::set_current_dir(&*CWD).unwrap();
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
let script = lua.load(script).eval::<LuaValue>().map_err(|e| {
std::env::set_current_dir(&*CWD).unwrap();
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
std::env::set_current_dir(&*CWD).unwrap();
let result = lua.load("return response").eval::<Table>().map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
// ------------------------------------------------------------------------
let script = if let LuaValue::String(script) = script {
script.to_string_lossy().to_string()
} else {
serde_json::to_string(&script).map_err(|e| {
eprintln!("Serde Error: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Serde error: {}", e),
)
})?
};
let mut response = Html(script).into_response();
if let Ok(status_code) = result.get::<Number>("code") {
if let Ok(code) = StatusCode::from_u16(status_code as u16) {
*response.status_mut() = code;
}
}
if let Ok(headers) = result.get::<Table>("headers") {
let pairs = headers.pairs::<String, String>();
for pair in pairs {
let (k, v) = pair.map_err(|e| {
eprintln!("Lua Error: {:?}", e);
render_lua_error(e)
})?;
response.headers_mut().insert(
HeaderName::from_str(&k).map_err(|e| {
eprintln!("Response Error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?,
HeaderValue::from_str(&v).map_err(|e| {
eprintln!("Response Error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?,
);
}
};
Ok(response)
}
#[allow(unused_variables)]
fn render_lua_error(e: mlua::Error) -> (StatusCode, String) {
match e {
mlua::Error::SyntaxError {
message,
incomplete_input,
} => {
let mut message = message.split("]:");
message.next();
(
StatusCode::INTERNAL_SERVER_ERROR,
message.next().unwrap_or_default().to_string(),
)
}
mlua::Error::RuntimeError(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
mlua::Error::MemoryError(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
mlua::Error::SafetyError(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
mlua::Error::MemoryControlNotAvailable => (
StatusCode::INTERNAL_SERVER_ERROR,
"MemoryControlNotAvailable".to_string(),
),
mlua::Error::RecursiveMutCallback => (
StatusCode::INTERNAL_SERVER_ERROR,
"RecursiveMutCallback".to_string(),
),
mlua::Error::CallbackDestructed => (
StatusCode::INTERNAL_SERVER_ERROR,
"CallbackDestructed".to_string(),
),
mlua::Error::StackError => (StatusCode::INTERNAL_SERVER_ERROR, "StackError".to_string()),
mlua::Error::BindError => (StatusCode::INTERNAL_SERVER_ERROR, "BindError".to_string()),
mlua::Error::BadArgument {
to,
pos,
name,
cause,
} => (
StatusCode::INTERNAL_SERVER_ERROR,
format!(
"Bad Argument:\n\tto: {}\n\tpos: {}\n\tname: {}\n\tcause: {:?}",
to.unwrap_or_default(),
pos,
name.unwrap_or_default(),
cause
),
),
mlua::Error::ToLuaConversionError { from, to, message } => (
StatusCode::INTERNAL_SERVER_ERROR,
message.unwrap_or_default(),
),
mlua::Error::FromLuaConversionError { from, to, message } => (
StatusCode::INTERNAL_SERVER_ERROR,
message.unwrap_or_default(),
),
mlua::Error::CoroutineUnresumable => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::UserDataTypeMismatch => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::UserDataDestructed => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::UserDataBorrowError => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::UserDataBorrowMutError => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::MetaMethodRestricted(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
mlua::Error::MetaMethodTypeError {
method,
type_name,
message,
} => (
StatusCode::INTERNAL_SERVER_ERROR,
message.unwrap_or_default(),
),
mlua::Error::MismatchedRegistryKey => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::CallbackError { traceback, cause } => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("{}\n\n{:#?}", traceback, cause),
),
mlua::Error::PreviouslyResumedPanic => (StatusCode::INTERNAL_SERVER_ERROR, String::new()),
mlua::Error::SerializeError(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
mlua::Error::DeserializeError(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
mlua::Error::ExternalError(error) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{:#?}", error))
}
mlua::Error::WithContext { context, cause } => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("{}\n\n{:#?}", context, cause),
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
String::from("Unknown Error"),
),
}
}
async fn handler(request: Request) -> Result<Html<String>, (StatusCode, String)> {
let start = Instant::now();
let mut path = BASE_DIR.clone();
let uri = urlencoding::decode(&request.uri().path()[1..]).map_err(|e| {
println!("{:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
path.push(&*uri);
let full_path = path.canonicalize().map_err(|e| {
println!("{:?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
let full_base_path = BASE_DIR.canonicalize().map_err(|e| {
println!("{:?}", e);
(StatusCode::NOT_FOUND, "404: Not Found".to_string())
})?;
if !full_path.starts_with(full_base_path) {
return Err((StatusCode::BAD_REQUEST, "400: Bad Request".to_string()));
}
let body = match crate::index::render_index(&path, start).await {
Ok(body) => Ok(body),
Err(e) => {
let ioerror: Option<&std::io::Error> = e.downcast_ref();
if let Some(ioerror) = ioerror {
if ioerror.kind() == ErrorKind::NotFound {
Err((StatusCode::NOT_FOUND, "404: Not Found".to_string()))
} else {
println!("{:?}", ioerror);
Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
} else {
println!("{:?}", e);
Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
}
};
Ok(Html(body?))
}