diff --git a/crates/lune-std-net/src/server/config.rs b/crates/lune-std-net/src/server/config.rs new file mode 100644 index 0000000..7421ca4 --- /dev/null +++ b/crates/lune-std-net/src/server/config.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use bstr::{BString, ByteSlice}; +use hyper::{header::CONTENT_TYPE, StatusCode}; +use mlua::prelude::*; + +use crate::shared::headers::table_to_hash_map; + +#[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() + )), + }) + } + } +} diff --git a/crates/lune-std-net/src/server/mod.rs b/crates/lune-std-net/src/server/mod.rs index e69de29..ef68c36 100644 --- a/crates/lune-std-net/src/server/mod.rs +++ b/crates/lune-std-net/src/server/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/crates/lune-std-net/src/shared/headers.rs b/crates/lune-std-net/src/shared/headers.rs index d87a8e5..c566d87 100644 --- a/crates/lune-std-net/src/shared/headers.rs +++ b/crates/lune-std-net/src/shared/headers.rs @@ -28,27 +28,50 @@ pub fn header_map_to_table( headers: HeaderMap, remove_content_headers: bool, ) -> LuaResult { - let mut res_headers = HashMap::>::new(); - for (name, value) in &headers { - let name = name.as_str(); - let value = value.to_str().unwrap().to_owned(); - if let Some(existing) = res_headers.get_mut(name) { - existing.push(value); - } else { - res_headers.insert(name.to_owned(), vec![value]); + let mut string_map = HashMap::>::new(); + + for (name, value) in headers { + if let Some(name) = name { + if let Ok(value) = value.to_str() { + string_map + .entry(name.to_string()) + .or_default() + .push(value.to_owned()); + } } } - if remove_content_headers { - let content_encoding_header_str = CONTENT_ENCODING.as_str(); - let content_length_header_str = CONTENT_LENGTH.as_str(); - res_headers.retain(|name, _| { - name != content_encoding_header_str && name != content_length_header_str - }); + hash_map_to_table(lua, string_map, remove_content_headers) +} + +pub fn hash_map_to_table( + lua: &Lua, + map: impl IntoIterator)>, + remove_content_headers: bool, +) -> LuaResult { + let mut string_map = HashMap::>::new(); + for (name, values) in map { + let name = name.as_str(); + + if remove_content_headers { + let content_encoding_header_str = CONTENT_ENCODING.as_str(); + let content_length_header_str = CONTENT_LENGTH.as_str(); + if name == content_encoding_header_str || name == content_length_header_str { + continue; + } + } + + for value in values { + let value = value.as_str(); + string_map + .entry(name.to_owned()) + .or_default() + .push(value.to_owned()); + } } let mut builder = TableBuilder::new(lua.clone())?; - for (name, mut values) in res_headers { + for (name, mut values) in string_map { if values.len() == 1 { let value = values.pop().unwrap().into_lua(lua)?; builder = builder.with_value(name, value)?; diff --git a/crates/lune-std-net/src/shared/request.rs b/crates/lune-std-net/src/shared/request.rs index 033831c..807a30d 100644 --- a/crates/lune-std-net/src/shared/request.rs +++ b/crates/lune-std-net/src/shared/request.rs @@ -1,15 +1,21 @@ -use http_body_util::Full; +use std::collections::HashMap; + +use futures_lite::prelude::*; +use http_body_util::{BodyStream, Full}; use url::Url; use hyper::{ - body::Bytes, + body::{Body as _, Bytes, Incoming}, header::{HeaderName, HeaderValue, USER_AGENT}, - HeaderMap, Request as HyperRequest, + HeaderMap, Method, Request as HyperRequest, }; use mlua::prelude::*; -use crate::{client::config::RequestConfig, shared::headers::create_user_agent_header}; +use crate::{ + client::config::RequestConfig, + shared::headers::{create_user_agent_header, hash_map_to_table, header_map_to_table}, +}; #[derive(Debug, Clone)] pub struct Request { @@ -72,6 +78,81 @@ impl Request { }) } + /** + Creates a new request from a raw incoming request. + */ + pub async fn from_incoming( + incoming: HyperRequest, + decompress: bool, + ) -> LuaResult { + let (parts, body) = incoming.into_parts(); + + let size = body.size_hint().lower() as usize; + let buffer = Vec::::with_capacity(size); + let body = BodyStream::new(body) + .try_fold(buffer, |mut body, chunk| { + if let Some(chunk) = chunk.data_ref() { + body.extend_from_slice(chunk); + } + Ok(body) + }) + .await + .into_lua_err()?; + + // TODO: Decompress body if decompress is true and headers are present + + Ok(Self { + inner: HyperRequest::from_parts(parts, Bytes::from(body)), + redirects: 0, + decompress, + }) + } + + /** + Returns the method of the request. + */ + pub fn method(&self) -> Method { + self.inner.method().clone() + } + + /** + Returns the path of the request. + */ + pub fn path(&self) -> &str { + self.inner.uri().path() + } + + /** + Returns the query parameters of the request. + */ + pub fn query(&self) -> HashMap> { + let uri = self.inner.uri(); + let url = uri.to_string().parse::().expect("uri is valid"); + + let mut result = HashMap::>::new(); + for (key, value) in url.query_pairs() { + result + .entry(key.into_owned()) + .or_default() + .push(value.into_owned()); + } + result + } + + /** + Returns the headers of the request. + */ + pub fn headers(&self) -> &HeaderMap { + self.inner.headers() + } + + /** + Returns the body of the request. + */ + pub fn body(&self) -> &[u8] { + self.inner.body() + } + /** Returns the inner `hyper` request with its body type modified to `Full` for sending. @@ -82,10 +163,10 @@ impl Request { .method(self.inner.method()) .uri(self.inner.uri()); - let headers = builder.headers_mut().expect("request was valid"); - for (name, value) in self.inner.headers() { - headers.insert(name, value.clone()); - } + builder + .headers_mut() + .expect("request was valid") + .extend(self.inner.headers().clone()); let body = Full::new(self.inner.body().clone()); builder.body(body).expect("request was valid") @@ -101,3 +182,17 @@ fn add_default_headers(lua: &Lua, headers: &mut HeaderMap) -> LuaResult<()> { Ok(()) } + +impl LuaUserData for Request { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("method", |_, this| Ok(this.method().to_string())); + fields.add_field_method_get("path", |_, this| Ok(this.path().to_string())); + fields.add_field_method_get("query", |lua, this| { + hash_map_to_table(lua, this.query(), false) + }); + fields.add_field_method_get("headers", |lua, this| { + header_map_to_table(lua, this.headers().clone(), this.decompress) + }); + fields.add_field_method_get("body", |lua, this| lua.create_string(this.body())); + } +} diff --git a/crates/lune-std-net/src/shared/response.rs b/crates/lune-std-net/src/shared/response.rs index d1256ac..c2cb61d 100644 --- a/crates/lune-std-net/src/shared/response.rs +++ b/crates/lune-std-net/src/shared/response.rs @@ -3,12 +3,13 @@ use http_body_util::{BodyStream, Full}; use hyper::{ body::{Body, Bytes, Incoming}, + header::{HeaderName, HeaderValue}, HeaderMap, Response as HyperResponse, }; use mlua::prelude::*; -use crate::shared::headers::header_map_to_table; +use crate::{server::config::ResponseConfig, shared::headers::header_map_to_table}; #[derive(Debug, Clone)] pub struct Response { @@ -19,12 +20,42 @@ pub struct Response { } impl Response { + /** + Creates a new response that is ready to be sent from a response configuration. + */ + pub fn from_config(config: ResponseConfig, _lua: Lua) -> LuaResult { + // 1. Create the inner response builder + let mut builder = HyperResponse::builder().status(config.status); + + // 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); + } + } + } + + // 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, + }) + } + /** Creates a new response from a raw incoming response. */ pub async fn from_incoming( incoming: HyperResponse, - decompressed: bool, + decompress: bool, ) -> LuaResult { let (parts, body) = incoming.into_parts(); @@ -40,12 +71,11 @@ impl Response { .await .into_lua_err()?; - let bytes = Bytes::from(body); - let inner = HyperResponse::from_parts(parts, bytes); + // TODO: Decompress body if decompress is true and headers are present Ok(Self { - inner, - decompressed, + inner: HyperResponse::from_parts(parts, Bytes::from(body)), + decompressed: decompress, }) } @@ -89,12 +119,14 @@ impl Response { type modified to `Full` for sending. */ pub fn as_full(&self) -> HyperResponse> { - let mut builder = HyperResponse::builder().version(self.inner.version()); + let mut builder = HyperResponse::builder() + .version(self.inner.version()) + .status(self.inner.status()); - let headers = builder.headers_mut().expect("request was valid"); - for (name, value) in self.inner.headers() { - headers.insert(name, value.clone()); - } + builder + .headers_mut() + .expect("request was valid") + .extend(self.inner.headers().clone()); let body = Full::new(self.inner.body().clone()); builder.body(body).expect("request was valid")