From 4725497f424cc2de017c6e3a53a1950b48621944 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sun, 27 Apr 2025 14:09:06 +0200 Subject: [PATCH] Initial working http server --- crates/lune-std-net/src/lib.rs | 9 ++- crates/lune-std-net/src/server/config.rs | 89 ++++++++++++++++++++++- crates/lune-std-net/src/server/mod.rs | 60 +++++++++++++++ crates/lune-std-net/src/server/service.rs | 52 +++++++++++++ 4 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 crates/lune-std-net/src/server/service.rs diff --git a/crates/lune-std-net/src/lib.rs b/crates/lune-std-net/src/lib.rs index 46282c3..25b4143 100644 --- a/crates/lune-std-net/src/lib.rs +++ b/crates/lune-std-net/src/lib.rs @@ -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 { 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 { 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), diff --git a/crates/lune-std-net/src/server/config.rs b/crates/lune-std-net/src/server/config.rs index 7421ca4..71ff5fa 100644 --- a/crates/lune-std-net/src/server/config.rs +++ b/crates/lune-std-net/src/server/config.rs @@ -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, +} + +impl FromLua for ServeConfig { + fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult { + 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 = t.get("address")?; + let handle_request: Option = t.get("handleRequest")?; + let handle_web_socket: Option = 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, + }) + } + } +} diff --git a/crates/lune-std-net/src/server/mod.rs b/crates/lune-std-net/src/server/mod.rs index ef68c36..4c330c7 100644 --- a/crates/lune-std-net/src/server/mod.rs +++ b/crates/lune-std-net/src/server/mod.rs @@ -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(()) +} diff --git a/crates/lune-std-net/src/server/service.rs b/crates/lune-std-net/src/server/service.rs new file mode 100644 index 0000000..8b527bb --- /dev/null +++ b/crates/lune-std-net/src/server/service.rs @@ -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> for Service { + type Response = HyperResponse>; + type Error = LuaError; + type Future = Pin>>>; + + fn call(&self, req: HyperRequest) -> 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()) + }) + } +}