use tiny_http::Method;
pub const MAX_BODY_BYTES: usize = 4 * 1024 * 1024;
pub struct BrokerRequest {
pub method: Method,
pub path: String,
pub query: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
pub peer_addr: Option<std::net::SocketAddr>,
}
impl BrokerRequest {
pub fn header(&self, name: &str) -> Option<String> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.clone())
}
pub fn is_loopback(&self) -> bool {
match self.peer_addr {
Some(a) => a.ip().is_loopback(),
None => true,
}
}
}
pub struct BrokerResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl BrokerResponse {
pub fn json_bytes(body: Vec<u8>, status: u16) -> Self {
BrokerResponse {
status,
headers: vec![("Content-Type".to_string(), "application/json".to_string())],
body,
}
}
pub fn with_header(mut self, k: &str, v: &str) -> Self {
self.headers.push((k.to_string(), v.to_string()));
self
}
}
impl BrokerRequest {
pub fn from_tiny_http(req: &mut tiny_http::Request) -> std::io::Result<Self> {
let method = req.method().clone();
let url = req.url().to_string();
let (path, query) = match url.split_once('?') {
Some((p, q)) => (p.to_string(), q.to_string()),
None => (url, String::new()),
};
let headers = req
.headers()
.iter()
.map(|h| {
(
h.field.as_str().as_str().to_string(),
h.value.as_str().to_string(),
)
})
.collect();
let peer_addr = req.remote_addr().copied();
let mut body = Vec::new();
req.as_reader().read_to_end(&mut body)?;
Ok(BrokerRequest {
method,
path,
query,
headers,
body,
peer_addr,
})
}
}
impl BrokerResponse {
pub fn into_tiny_http(self) -> tiny_http::Response<std::io::Cursor<Vec<u8>>> {
let mut r = tiny_http::Response::from_data(self.body).with_status_code(self.status);
for (k, v) in self.headers {
if let Ok(h) = tiny_http::Header::from_bytes(k.as_bytes(), v.as_bytes()) {
r = r.with_header(h);
}
}
r
}
}
impl BrokerRequest {
pub async fn from_axum(
req: axum::extract::Request,
peer_addr: Option<std::net::SocketAddr>,
) -> Self {
let method = match *req.method() {
axum::http::Method::GET => Method::Get,
axum::http::Method::POST => Method::Post,
axum::http::Method::DELETE => Method::Delete,
axum::http::Method::PUT => Method::Put,
axum::http::Method::HEAD => Method::Head,
axum::http::Method::PATCH => Method::Patch,
axum::http::Method::OPTIONS => Method::Options,
axum::http::Method::CONNECT => Method::Connect,
axum::http::Method::TRACE => Method::Trace,
_ => Method::Get,
};
let uri = req.uri();
let path = uri.path().to_string();
let query = uri.query().unwrap_or("").to_string();
let headers = req
.headers()
.iter()
.map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body = match axum::body::to_bytes(req.into_body(), MAX_BODY_BYTES + 1).await {
Ok(b) => b.to_vec(),
Err(_) => vec![0u8; MAX_BODY_BYTES + 1],
};
BrokerRequest {
method,
path,
query,
headers,
body,
peer_addr,
}
}
}
impl axum::response::IntoResponse for BrokerResponse {
fn into_response(self) -> axum::response::Response {
use axum::http::{HeaderName, HeaderValue, StatusCode};
let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let mut response = axum::response::Response::new(axum::body::Body::from(self.body));
*response.status_mut() = status;
let headers = response.headers_mut();
for (k, v) in self.headers {
if let (Ok(name), Ok(val)) = (
HeaderName::from_bytes(k.as_bytes()),
HeaderValue::from_str(&v),
) {
headers.append(name, val);
}
}
response
}
}
#[cfg(test)]
mod tests {
use super::*;
fn req(headers: Vec<(&str, &str)>, peer: Option<&str>) -> BrokerRequest {
BrokerRequest {
method: Method::Get,
path: "/x".into(),
query: String::new(),
headers: headers
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
body: vec![],
peer_addr: peer.map(|s| s.parse().unwrap()),
}
}
#[test]
fn header_is_case_insensitive() {
let r = req(vec![("X-Api-Key", "abc")], None);
assert_eq!(r.header("x-api-key").as_deref(), Some("abc"));
assert_eq!(r.header("X-Api-Key").as_deref(), Some("abc"));
assert_eq!(r.header("missing"), None);
}
#[test]
fn is_loopback_matches_127_and_unknown() {
assert!(req(vec![], Some("127.0.0.1:5000")).is_loopback());
assert!(req(vec![], None).is_loopback()); assert!(!req(vec![], Some("10.0.0.5:5000")).is_loopback());
}
#[test]
fn json_bytes_sets_content_type() {
let r = BrokerResponse::json_bytes(b"{}".to_vec(), 200);
assert_eq!(r.status, 200);
assert!(
r.headers
.iter()
.any(|(k, v)| k.eq_ignore_ascii_case("content-type")
&& v.contains("application/json"))
);
}
#[test]
fn with_header_appends() {
let r = BrokerResponse::json_bytes(vec![], 200).with_header("X-Test", "1");
assert!(r.headers.iter().any(|(k, v)| k == "X-Test" && v == "1"));
}
#[test]
fn into_tiny_http_preserves_status_and_headers() {
let resp = BrokerResponse::json_bytes(b"{\"ok\":true}".to_vec(), 201)
.with_header("X-Zakuro-Cost", "0.50");
let th = resp.into_tiny_http();
assert_eq!(th.status_code().0, 201);
let headers: Vec<(String, String)> = th
.headers()
.iter()
.map(|h| {
(
h.field.as_str().as_str().to_string(),
h.value.as_str().to_string(),
)
})
.collect();
assert!(
headers
.iter()
.any(|(k, v)| k.eq_ignore_ascii_case("content-type")
&& v.contains("application/json"))
);
assert!(headers
.iter()
.any(|(k, v)| k == "X-Zakuro-Cost" && v == "0.50"));
}
#[test]
fn into_tiny_http_skips_invalid_headers() {
let resp = BrokerResponse {
status: 200,
headers: vec![
("X-Good".to_string(), "ok".to_string()),
("X-Bad".to_string(), "wörker\u{00ff}".to_string()),
],
body: Vec::new(),
};
let th = resp.into_tiny_http();
let headers: Vec<String> = th
.headers()
.iter()
.map(|h| h.field.as_str().as_str().to_string())
.collect();
assert!(headers.iter().any(|k| k == "X-Good"));
assert!(!headers.iter().any(|k| k == "X-Bad"));
}
#[test]
fn response_round_trips_through_tiny_http() {
let body = b"hello-bytes".to_vec();
let resp = BrokerResponse {
status: 200,
headers: vec![(
"Content-Type".to_string(),
"application/octet-stream".to_string(),
)],
body: body.clone(),
};
let th = resp.into_tiny_http();
assert_eq!(th.data_length(), Some(body.len()));
}
}