diff --git a/.lune/http_server.luau b/.lune/http_server.luau index 9d6fa73..b3ad573 100644 --- a/.lune/http_server.luau +++ b/.lune/http_server.luau @@ -3,7 +3,6 @@ local net = require("@lune/net") local process = require("@lune/process") -local task = require("@lune/task") local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0 then assert(tonumber(process.env.PORT), "Failed to parse port from env") @@ -11,6 +10,10 @@ local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0 -- Create our responder functions +local function root(_request: net.ServeRequest): string + return `Hello from Lune server!` +end + local function pong(request: net.ServeRequest): string return `Pong!\n{request.path}\n{request.body}` end @@ -29,10 +32,12 @@ local function notFound(_request: net.ServeRequest): net.ServeResponse } end --- Run the server on port 8080 +-- Run the server on the port forever -local handle = net.serve(PORT, function(request) - if string.sub(request.path, 1, 5) == "/ping" then +net.serve(PORT, function(request) + if request.path == "/" then + return root(request) + elseif string.sub(request.path, 1, 5) == "/ping" then return pong(request) elseif string.sub(request.path, 1, 7) == "/teapot" then return teapot(request) @@ -42,12 +47,4 @@ local handle = net.serve(PORT, function(request) end) print(`Listening on port {PORT} 🚀`) - --- Exit our example after a small delay, if you copy this --- example just remove this part to keep the server running - -task.delay(2, function() - print("Shutting down...") - task.wait(1) - handle:stop() -end) +print("Press Ctrl+C to stop") diff --git a/crates/lune-std-net/src/client/config.rs b/crates/lune-std-net/src/client/config.rs deleted file mode 100644 index d0668b4..0000000 --- a/crates/lune-std-net/src/client/config.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::collections::HashMap; - -use bstr::{BString, ByteSlice}; -use hyper::{header::USER_AGENT, Method}; -use mlua::prelude::*; - -use crate::shared::headers::{create_user_agent_header, table_to_hash_map}; - -#[derive(Debug, Clone)] -pub struct RequestConfigOptions { - pub decompress: bool, -} - -impl Default for RequestConfigOptions { - fn default() -> Self { - Self { decompress: true } - } -} - -impl FromLua for RequestConfigOptions { - fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { - if let LuaValue::Nil = value { - // Nil means default options - Ok(Self::default()) - } else if let LuaValue::Table(tab) = value { - // Table means custom options - let decompress = match tab.get::>("decompress") { - Ok(decomp) => Ok(decomp.unwrap_or(true)), - Err(_) => Err(LuaError::RuntimeError( - "Invalid option value for 'decompress' in request config options".to_string(), - )), - }?; - Ok(Self { decompress }) - } else { - // Anything else is invalid - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "RequestConfigOptions".to_string(), - message: Some(format!( - "Invalid request config options - expected table or nil, got {}", - value.type_name() - )), - }) - } - } -} - -#[derive(Debug, Clone)] -pub struct RequestConfig { - pub url: String, - pub method: Method, - pub query: HashMap>, - pub headers: HashMap>, - pub body: Option>, - pub options: RequestConfigOptions, -} - -impl FromLua for RequestConfig { - fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult { - // If we just got a string we assume its a GET request to a given url - if let LuaValue::String(s) = value { - Ok(Self { - url: s.to_string_lossy().to_string(), - method: Method::GET, - query: HashMap::new(), - headers: HashMap::new(), - body: None, - options: RequestConfigOptions::default(), - }) - } else if let LuaValue::Table(tab) = value { - // If we got a table we are able to configure the entire request - - // Extract url - let url = match tab.get::("url") { - Ok(config_url) => Ok(config_url.to_string_lossy().to_string()), - Err(_) => Err(LuaError::runtime("Missing 'url' in request config")), - }?; - // Extract method - let method = match tab.get::("method") { - Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(), - Err(_) => "GET".to_string(), - }; - // Extract query - let query = match tab.get::("query") { - Ok(tab) => table_to_hash_map(tab, "query")?, - Err(_) => HashMap::new(), - }; - // Extract headers - let mut headers = match tab.get::("headers") { - Ok(tab) => table_to_hash_map(tab, "headers")?, - Err(_) => HashMap::new(), - }; - // Extract body - let body = match tab.get::("body") { - Ok(config_body) => Some(config_body.as_bytes().to_owned()), - Err(_) => None, - }; - - // Convert method string into proper enum - let method = method.trim().to_ascii_uppercase(); - let method = match method.as_ref() { - "GET" => Ok(Method::GET), - "POST" => Ok(Method::POST), - "PUT" => Ok(Method::PUT), - "DELETE" => Ok(Method::DELETE), - "HEAD" => Ok(Method::HEAD), - "OPTIONS" => Ok(Method::OPTIONS), - "PATCH" => Ok(Method::PATCH), - _ => Err(LuaError::RuntimeError(format!( - "Invalid request config method '{}'", - &method - ))), - }?; - - // Parse any extra options given - let options = match tab.get::("options") { - Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?, - Err(_) => RequestConfigOptions::default(), - }; - - // Finally, add any default headers, if applicable - add_default_headers(lua, &mut headers)?; - - // All good, validated and we got what we need - Ok(Self { - url, - method, - query, - headers, - body, - options, - }) - } else { - // Anything else is invalid - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "RequestConfig".to_string(), - message: Some(format!( - "Invalid request config - expected string or table, got {}", - value.type_name() - )), - }) - } - } -} - -fn add_default_headers(lua: &Lua, headers: &mut HashMap>) -> LuaResult<()> { - if !headers.contains_key(USER_AGENT.as_str()) { - let ua = create_user_agent_header(lua)?; - headers.insert(USER_AGENT.to_string(), vec![ua]); - } - - Ok(()) -} diff --git a/crates/lune-std-net/src/client/mod.rs b/crates/lune-std-net/src/client/mod.rs index 743d9d3..b0a6cf9 100644 --- a/crates/lune-std-net/src/client/mod.rs +++ b/crates/lune-std-net/src/client/mod.rs @@ -18,7 +18,6 @@ use crate::{ }, }; -pub mod config; pub mod http_stream; pub mod rustls; pub mod ws_stream; diff --git a/crates/lune-std-net/src/lib.rs b/crates/lune-std-net/src/lib.rs index 4ef9839..b8b4532 100644 --- a/crates/lune-std-net/src/lib.rs +++ b/crates/lune-std-net/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::cargo_common_metadata)] +use hyper::header::{HeaderValue, USER_AGENT}; use lune_utils::TableBuilder; use mlua::prelude::*; @@ -9,9 +10,12 @@ pub(crate) mod shared; pub(crate) mod url; use self::{ - client::{config::RequestConfig, ws_stream::WsStream}, + client::ws_stream::WsStream, server::{config::ServeConfig, handle::ServeHandle}, - shared::{request::Request, response::Response, websocket::Websocket}, + shared::{ + headers::create_user_agent_header, request::Request, response::Response, + websocket::Websocket, + }, }; const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau")); @@ -41,8 +45,13 @@ pub fn module(lua: Lua) -> LuaResult { .build_readonly() } -async fn net_request(lua: Lua, config: RequestConfig) -> LuaResult { - self::client::send_request(Request::try_from(config)?, lua).await +async fn net_request(lua: Lua, mut req: Request) -> LuaResult { + if !req.headers().contains_key(USER_AGENT.as_str()) { + let ua = create_user_agent_header(&lua)?; + let ua = HeaderValue::from_str(&ua).into_lua_err()?; + req.inner.headers_mut().insert(USER_AGENT, ua); + } + self::client::send_request(req, lua).await } async fn net_socket(_: Lua, url: String) -> LuaResult> { diff --git a/crates/lune-std-net/src/server/config.rs b/crates/lune-std-net/src/server/config.rs index 71ff5fa..06fe6a8 100644 --- a/crates/lune-std-net/src/server/config.rs +++ b/crates/lune-std-net/src/server/config.rs @@ -1,14 +1,7 @@ -use std::{ - collections::HashMap, - net::{IpAddr, Ipv4Addr}, -}; +use std::net::{IpAddr, Ipv4Addr}; -use bstr::{BString, ByteSlice}; -use hyper::{header::CONTENT_TYPE, StatusCode}; 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#" @@ -21,64 +14,6 @@ return { } "#; -#[derive(Debug, Clone)] -pub struct ResponseConfig { - pub status: StatusCode, - pub headers: HashMap>, - pub body: Option>, -} - -impl FromLua for ResponseConfig { - fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { - // If we just got a string we assume its a plaintext 200 response - if let LuaValue::String(s) = value { - Ok(Self { - status: StatusCode::OK, - headers: HashMap::from([( - CONTENT_TYPE.to_string(), - vec!["text/plain".to_string()], - )]), - body: Some(s.as_bytes().to_owned()), - }) - } else if let LuaValue::Table(tab) = value { - // If we got a table we are able to configure the entire response - - // Extract url - let status = match tab.get::("status") { - Ok(status) => Ok(StatusCode::from_u16(status).into_lua_err()?), - Err(_) => Err(LuaError::runtime("Missing 'status' in response config")), - }?; - // Extract headers - let headers = match tab.get::("headers") { - Ok(tab) => table_to_hash_map(tab, "headers")?, - Err(_) => HashMap::new(), - }; - // Extract body - let body = match tab.get::("body") { - Ok(config_body) => Some(config_body.as_bytes().to_owned()), - Err(_) => None, - }; - - // All good, validated and we got what we need - Ok(Self { - status, - headers, - body, - }) - } else { - // Anything else is invalid - Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "ResponseConfig".to_string(), - message: Some(format!( - "Invalid response config - expected string or table, got {}", - value.type_name() - )), - }) - } - } -} - #[derive(Debug, Clone)] pub struct ServeConfig { pub address: IpAddr, diff --git a/crates/lune-std-net/src/server/service.rs b/crates/lune-std-net/src/server/service.rs index 1b416d3..31f7d30 100644 --- a/crates/lune-std-net/src/server/service.rs +++ b/crates/lune-std-net/src/server/service.rs @@ -13,7 +13,7 @@ use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt}; use crate::{ server::{ - config::{ResponseConfig, ServeConfig}, + config::ServeConfig, upgrade::{is_upgrade_request, make_upgrade_response}, }, shared::{hyper::HyperIo, request::Request, response::Response, websocket::Websocket}, @@ -96,8 +96,7 @@ async fn handle_request( .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)?; + let response = Response::from_lua_multi(thread_res, &lua)?; Ok(response.into_full()) } diff --git a/crates/lune-std-net/src/shared/headers.rs b/crates/lune-std-net/src/shared/headers.rs index c566d87..de0db54 100644 --- a/crates/lune-std-net/src/shared/headers.rs +++ b/crates/lune-std-net/src/shared/headers.rs @@ -86,33 +86,3 @@ pub fn hash_map_to_table( builder.build_readonly() } - -pub fn table_to_hash_map( - tab: LuaTable, - tab_origin_key: &'static str, -) -> LuaResult>> { - let mut map = HashMap::new(); - - for pair in tab.pairs::() { - let (key, value) = pair?; - match value { - LuaValue::String(s) => { - map.insert(key, vec![s.to_str()?.to_owned()]); - } - LuaValue::Table(t) => { - let mut values = Vec::new(); - for value in t.sequence_values::() { - values.push(value?.to_str()?.to_owned()); - } - map.insert(key, values); - } - _ => { - return Err(LuaError::runtime(format!( - "Value for '{tab_origin_key}' must be a string or array of strings", - ))) - } - } - } - - Ok(map) -} diff --git a/crates/lune-std-net/src/shared/lua.rs b/crates/lune-std-net/src/shared/lua.rs new file mode 100644 index 0000000..1f81f96 --- /dev/null +++ b/crates/lune-std-net/src/shared/lua.rs @@ -0,0 +1,57 @@ +use hyper::{ + body::Bytes, + header::{HeaderName, HeaderValue}, + HeaderMap, Method, +}; +use mlua::prelude::*; + +pub fn lua_value_to_bytes(value: &LuaValue) -> LuaResult { + match value { + LuaValue::Nil => Ok(Bytes::new()), + LuaValue::Buffer(buf) => Ok(Bytes::from(buf.to_vec())), + LuaValue::String(str) => Ok(Bytes::copy_from_slice(&str.as_bytes())), + v => Err(LuaError::FromLuaConversionError { + from: v.type_name(), + to: "Bytes".to_string(), + message: Some(format!( + "Invalid body - expected string or buffer, got {}", + v.type_name() + )), + }), + } +} + +pub fn lua_value_to_method(value: &LuaValue) -> LuaResult { + match value { + LuaValue::Nil => Ok(Method::GET), + LuaValue::String(str) => { + let bytes = str.as_bytes().trim_ascii().to_ascii_uppercase(); + Method::from_bytes(&bytes).into_lua_err() + } + LuaValue::Buffer(buf) => { + let bytes = buf.to_vec().trim_ascii().to_ascii_uppercase(); + Method::from_bytes(&bytes).into_lua_err() + } + v => Err(LuaError::FromLuaConversionError { + from: v.type_name(), + to: "Method".to_string(), + message: Some(format!( + "Invalid method - expected string or buffer, got {}", + v.type_name() + )), + }), + } +} + +pub fn lua_table_to_header_map(table: &LuaTable) -> LuaResult { + let mut headers = HeaderMap::new(); + + for pair in table.pairs::() { + let (key, val) = pair?; + let key = HeaderName::from_bytes(&key.as_bytes()).into_lua_err()?; + let val = HeaderValue::from_bytes(&val.as_bytes()).into_lua_err()?; + headers.insert(key, val); + } + + Ok(headers) +} diff --git a/crates/lune-std-net/src/shared/mod.rs b/crates/lune-std-net/src/shared/mod.rs index 4c02f02..733fa69 100644 --- a/crates/lune-std-net/src/shared/mod.rs +++ b/crates/lune-std-net/src/shared/mod.rs @@ -2,6 +2,7 @@ pub mod futures; pub mod headers; pub mod hyper; pub mod incoming; +pub mod lua; pub mod request; pub mod response; pub mod websocket; diff --git a/crates/lune-std-net/src/shared/request.rs b/crates/lune-std-net/src/shared/request.rs index 5bdbd75..8f3859e 100644 --- a/crates/lune-std-net/src/shared/request.rs +++ b/crates/lune-std-net/src/shared/request.rs @@ -5,20 +5,56 @@ use url::Url; use hyper::{ body::{Bytes, Incoming}, - header::{HeaderName, HeaderValue}, HeaderMap, Method, Request as HyperRequest, }; use mlua::prelude::*; -use crate::{ - client::config::RequestConfig, - shared::{ - headers::{hash_map_to_table, header_map_to_table}, - incoming::handle_incoming_body, - }, +use crate::shared::{ + headers::{hash_map_to_table, header_map_to_table}, + incoming::handle_incoming_body, + lua::{lua_table_to_header_map, lua_value_to_bytes, lua_value_to_method}, }; +#[derive(Debug, Clone)] +pub struct RequestOptions { + pub decompress: bool, +} + +impl Default for RequestOptions { + fn default() -> Self { + Self { decompress: true } + } +} + +impl FromLua for RequestOptions { + fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { + if let LuaValue::Nil = value { + // Nil means default options + Ok(Self::default()) + } else if let LuaValue::Table(tab) = value { + // Table means custom options + let decompress = match tab.get::>("decompress") { + Ok(decomp) => Ok(decomp.unwrap_or(true)), + Err(_) => Err(LuaError::RuntimeError( + "Invalid option value for 'decompress' in request options".to_string(), + )), + }?; + Ok(Self { decompress }) + } else { + // Anything else is invalid + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "RequestOptions".to_string(), + message: Some(format!( + "Invalid request options - expected table or nil, got {}", + value.type_name() + )), + }) + } + } +} + #[derive(Debug, Clone)] pub struct Request { // NOTE: We use Bytes instead of Full to avoid @@ -135,52 +171,83 @@ impl Request { } } -impl TryFrom for Request { - type Error = LuaError; - fn try_from(config: RequestConfig) -> Result { - // 1. Parse the URL and make sure it is valid - let mut url = Url::parse(&config.url).into_lua_err()?; +impl FromLua for Request { + fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult { + if let LuaValue::String(s) = value { + // If we just got a string we assume + // its a GET request to a given url + let uri = s.to_str()?; + let uri = uri.parse().into_lua_err()?; - // 2. Append any query pairs passed as a table - { - let mut query = url.query_pairs_mut(); - for (key, values) in config.query { - for value in values { + let mut request = HyperRequest::new(Bytes::new()); + *request.uri_mut() = uri; + + Ok(Self { + inner: request, + address: None, + redirects: None, + decompress: RequestOptions::default().decompress, + }) + } else if let LuaValue::Table(tab) = value { + // If we got a table we are able to configure the + // entire request, maybe with extra options too + let options = match tab.get::("options") { + Ok(opts) => RequestOptions::from_lua(opts, lua)?, + Err(_) => RequestOptions::default(), + }; + + // Extract url (required) + optional structured query params + let url = tab.get::("url")?; + let mut url = url.to_str()?.parse::().into_lua_err()?; + if let Some(t) = tab.get::>("query")? { + let mut query = url.query_pairs_mut(); + for pair in t.pairs::() { + let (key, value) = pair?; + let key = key.to_str()?; + let value = value.to_str()?; query.append_pair(&key, &value); } } + + // Extract method + let method = tab.get::("method")?; + let method = lua_value_to_method(&method)?; + + // Extract headers + let headers = tab.get::>("headers")?; + let headers = headers + .map(|t| lua_table_to_header_map(&t)) + .transpose()? + .unwrap_or_default(); + + // Extract body + let body = tab.get::("body")?; + let body = lua_value_to_bytes(&body)?; + + // Build the full request + let mut request = HyperRequest::new(body); + request.headers_mut().extend(headers); + *request.uri_mut() = url.to_string().parse().unwrap(); + *request.method_mut() = method; + + // All good, validated and we got what we need + Ok(Self { + inner: request, + address: None, + redirects: None, + decompress: options.decompress, + }) + } else { + // Anything else is invalid + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "Request".to_string(), + message: Some(format!( + "Invalid request - expected string or table, got {}", + value.type_name() + )), + }) } - - // 3. Create the inner request builder - let mut builder = HyperRequest::builder() - .method(config.method) - .uri(url.as_str()); - - // 4. Append any headers passed as a table - builder - // headers may be None if builder is already invalid - if let Some(headers) = builder.headers_mut() { - for (key, values) in config.headers { - let key = HeaderName::from_bytes(key.as_bytes()).into_lua_err()?; - for value in values { - let value = HeaderValue::from_str(&value).into_lua_err()?; - headers.insert(key.clone(), value); - } - } - } - - // 5. Convert request body bytes to the proper Body - // type that Hyper expects, if we got any bytes - let body = config.body.map(Bytes::from).unwrap_or_default(); - - // 6. Finally, attach the body, verifying that the request is valid - let inner = builder.body(body).into_lua_err()?; - - Ok(Self { - inner, - address: None, - redirects: None, - decompress: config.options.decompress, - }) } } diff --git a/crates/lune-std-net/src/shared/response.rs b/crates/lune-std-net/src/shared/response.rs index 9c576e2..6b40339 100644 --- a/crates/lune-std-net/src/shared/response.rs +++ b/crates/lune-std-net/src/shared/response.rs @@ -2,15 +2,16 @@ use http_body_util::Full; use hyper::{ body::{Bytes, Incoming}, - header::{HeaderName, HeaderValue}, - HeaderMap, Response as HyperResponse, + header::{HeaderValue, CONTENT_TYPE}, + HeaderMap, Response as HyperResponse, StatusCode, }; use mlua::prelude::*; -use crate::{ - server::config::ResponseConfig, - shared::{headers::header_map_to_table, incoming::handle_incoming_body}, +use crate::shared::{ + headers::header_map_to_table, + incoming::handle_incoming_body, + lua::{lua_table_to_header_map, lua_value_to_bytes}, }; #[derive(Debug, Clone)] @@ -104,33 +105,55 @@ impl Response { } } -impl TryFrom for Response { - type Error = LuaError; - fn try_from(config: ResponseConfig) -> Result { - // 1. Create the inner response builder - let mut builder = HyperResponse::builder().status(config.status); +impl FromLua for Response { + fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { + if let Ok(body) = lua_value_to_bytes(&value) { + // String or buffer is always a 200 text/plain response + let mut response = HyperResponse::new(body); + response + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); + Ok(Self { + inner: response, + decompressed: false, + }) + } else if let LuaValue::Table(tab) = value { + // Extract status (required) + let status = tab.get::("status")?; + let status = StatusCode::from_u16(status).into_lua_err()?; - // 2. Append any headers passed as a table - builder - // headers may be None if builder is already invalid - if let Some(headers) = builder.headers_mut() { - for (key, values) in config.headers { - let key = HeaderName::from_bytes(key.as_bytes()).into_lua_err()?; - for value in values { - let value = HeaderValue::from_str(&value).into_lua_err()?; - headers.insert(key.clone(), value); - } - } + // Extract headers + let headers = tab.get::>("headers")?; + let headers = headers + .map(|t| lua_table_to_header_map(&t)) + .transpose()? + .unwrap_or_default(); + + // Extract body + let body = tab.get::("body")?; + let body = lua_value_to_bytes(&body)?; + + // Build the full response + let mut response = HyperResponse::new(body); + response.headers_mut().extend(headers); + *response.status_mut() = status; + + // All good, validated and we got what we need + Ok(Self { + inner: response, + decompressed: false, + }) + } else { + // Anything else is invalid + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "Response".to_string(), + message: Some(format!( + "Invalid response - expected table/string/buffer, got {}", + value.type_name() + )), + }) } - - // 3. Convert response body bytes to the proper Body - // type that Hyper expects, if we got any bytes - let body = config.body.map(Bytes::from).unwrap_or_default(); - - // 4. Finally, attach the body, verifying that the response is valid - Ok(Self { - inner: builder.body(body).into_lua_err()?, - decompressed: false, - }) } }