Initial working http server

This commit is contained in:
Filip Tibell 2025-04-27 14:09:06 +02:00
parent 9157ed9d33
commit 4725497f42
4 changed files with 206 additions and 4 deletions

View file

@ -8,10 +8,9 @@ pub(crate) mod server;
pub(crate) mod shared;
pub(crate) mod url;
#[allow(unused_imports)]
use self::{
client::config::RequestConfig,
server::config::ResponseConfig,
server::config::ServeConfig,
shared::{request::Request, response::Response},
};
@ -36,7 +35,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_async_function("request", net_request)?
// .with_async_function("socket", net_socket)?
// .with_async_function("serve", net_serve)?
.with_async_function("serve", net_serve)?
.with_function("urlEncode", net_url_encode)?
.with_function("urlDecode", net_url_decode)?
.build_readonly()
@ -46,6 +45,10 @@ async fn net_request(lua: Lua, config: RequestConfig) -> LuaResult<Response> {
self::client::send_request(Request::try_from(config)?, lua).await
}
async fn net_serve(lua: Lua, (port, config): (u16, ServeConfig)) -> LuaResult<()> {
self::server::serve(lua, port, config).await
}
fn net_url_encode(
lua: &Lua,
(lua_string, as_binary): (LuaString, Option<bool>),

View file

@ -1,4 +1,7 @@
use std::collections::HashMap;
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr},
};
use bstr::{BString, ByteSlice};
use hyper::{header::CONTENT_TYPE, StatusCode};
@ -6,6 +9,18 @@ use mlua::prelude::*;
use crate::shared::headers::table_to_hash_map;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
#[derive(Debug, Clone)]
pub struct ResponseConfig {
pub status: StatusCode,
@ -63,3 +78,75 @@ impl FromLua for ResponseConfig {
}
}
}
#[derive(Debug, Clone)]
pub struct ServeConfig {
pub address: IpAddr,
pub handle_request: LuaFunction,
pub handle_web_socket: Option<LuaFunction>,
}
impl FromLua for ServeConfig {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
if let LuaValue::Function(f) = &value {
// Single function = request handler, rest is default
Ok(ServeConfig {
handle_request: f.clone(),
handle_web_socket: None,
address: DEFAULT_IP_ADDRESS,
})
} else if let LuaValue::Table(t) = &value {
// Table means custom options
let address: Option<LuaString> = t.get("address")?;
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = t.get("handleWebSocket")?;
if handle_request.is_some() || handle_web_socket.is_some() {
let address: IpAddr = match &address {
Some(addr) => {
let addr_str = addr.to_str()?;
addr_str
.trim_start_matches("http://")
.trim_start_matches("https://")
.parse()
.map_err(|_e| LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig".to_string(),
message: Some(format!(
"IP address format is incorrect - \
expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \
got '{addr_str}'"
)),
})?
}
None => DEFAULT_IP_ADDRESS,
};
Ok(Self {
address,
handle_request: handle_request.unwrap_or_else(|| {
lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER)
.into_function()
.expect("Failed to create default http responder function")
}),
handle_web_socket,
})
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig".to_string(),
message: Some(String::from(
"Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function",
)),
})
}
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig".to_string(),
message: None,
})
}
}
}

View file

@ -1 +1,61 @@
use std::net::SocketAddr;
use async_net::TcpListener;
use hyper::server::conn::http1::Builder as Http1Builder;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use crate::{
server::{config::ServeConfig, service::Service},
shared::hyper::{HyperIo, HyperTimer},
};
pub mod config;
pub mod service;
/**
Starts an HTTP server using the given port and configuration.
*/
pub async fn serve(lua: Lua, port: u16, config: ServeConfig) -> LuaResult<()> {
let address = SocketAddr::from((config.address, port));
let service = Service {
lua: lua.clone(),
address,
config,
};
let listener = TcpListener::bind(address).await?;
lua.spawn_local({
let lua = lua.clone();
async move {
loop {
let (connection, _addr) = match listener.accept().await {
Ok((connection, addr)) => (connection, addr),
Err(err) => {
eprintln!("Error while accepting connection: {err}");
continue;
}
};
lua.spawn_local({
let service = service.clone();
async move {
let result = Http1Builder::new()
.timer(HyperTimer)
.keep_alive(true) // Needed for websockets
.serve_connection(HyperIo::from(connection), service)
.with_upgrades()
.await;
if let Err(err) = result {
eprintln!("Error while responding to request: {err}");
}
}
});
}
}
});
Ok(())
}

View file

@ -0,0 +1,52 @@
use std::{future::Future, net::SocketAddr, pin::Pin};
use http_body_util::Full;
use hyper::{
body::{Bytes, Incoming},
service::Service as HyperService,
Request as HyperRequest, Response as HyperResponse,
};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSchedulerExt;
use crate::{
server::config::{ResponseConfig, ServeConfig},
shared::{request::Request, response::Response},
};
#[derive(Debug, Clone)]
pub(super) struct Service {
pub(super) lua: Lua,
pub(super) address: SocketAddr,
pub(super) config: ServeConfig,
}
impl HyperService<HyperRequest<Incoming>> for Service {
type Response = HyperResponse<Full<Bytes>>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn call(&self, req: HyperRequest<Incoming>) -> Self::Future {
let lua = self.lua.clone();
let config = self.config.clone();
Box::pin(async move {
let handler = config.handle_request.clone();
let request = Request::from_incoming(req, true).await?;
let thread_id = lua.push_thread_back(handler, request)?;
lua.track_thread(thread_id);
lua.wait_for_thread(thread_id).await;
let thread_res = lua
.get_thread_result(thread_id)
.expect("Missing handler thread result")?;
let config = ResponseConfig::from_lua_multi(thread_res, &lua)?;
let response = Response::try_from(config)?;
Ok(response.as_full())
})
}
}