mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Implement server response config and full symmetry between request and response methods
This commit is contained in:
parent
1e4b020d84
commit
a0f55bc1ec
5 changed files with 250 additions and 34 deletions
65
crates/lune-std-net/src/server/config.rs
Normal file
65
crates/lune-std-net/src/server/config.rs
Normal file
|
@ -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<String, Vec<String>>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl FromLua for ResponseConfig {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
// 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::<u16>("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::<LuaTable>("headers") {
|
||||
Ok(tab) => table_to_hash_map(tab, "headers")?,
|
||||
Err(_) => HashMap::new(),
|
||||
};
|
||||
// Extract body
|
||||
let body = match tab.get::<BString>("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()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod config;
|
|
@ -28,27 +28,50 @@ pub fn header_map_to_table(
|
|||
headers: HeaderMap,
|
||||
remove_content_headers: bool,
|
||||
) -> LuaResult<LuaTable> {
|
||||
let mut res_headers = HashMap::<String, Vec<String>>::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::<String, Vec<String>>::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<Item = (String, Vec<String>)>,
|
||||
remove_content_headers: bool,
|
||||
) -> LuaResult<LuaTable> {
|
||||
let mut string_map = HashMap::<String, Vec<String>>::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)?;
|
||||
|
|
|
@ -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<Incoming>,
|
||||
decompress: bool,
|
||||
) -> LuaResult<Self> {
|
||||
let (parts, body) = incoming.into_parts();
|
||||
|
||||
let size = body.size_hint().lower() as usize;
|
||||
let buffer = Vec::<u8>::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<String, Vec<String>> {
|
||||
let uri = self.inner.uri();
|
||||
let url = uri.to_string().parse::<Url>().expect("uri is valid");
|
||||
|
||||
let mut result = HashMap::<String, Vec<String>>::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<Bytes>` 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<F: LuaUserDataFields<Self>>(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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Self> {
|
||||
// 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<Incoming>,
|
||||
decompressed: bool,
|
||||
decompress: bool,
|
||||
) -> LuaResult<Self> {
|
||||
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<Bytes>` for sending.
|
||||
*/
|
||||
pub fn as_full(&self) -> HyperResponse<Full<Bytes>> {
|
||||
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")
|
||||
|
|
Loading…
Add table
Reference in a new issue