use std::ffi::CString;
pub struct WorkerRequestParams<'a> {
pub request_body: Vec<u8>,
pub cookie_header: Option<&'a str>,
pub request_headers: Vec<(String, String)>,
pub method: &'a str,
pub uri: &'a str,
pub query_string: &'a str,
pub content_type: Option<&'a str>,
pub remote_addr: Option<&'a str>,
pub server_name: Option<&'a str>,
pub server_port: u16,
pub https: bool,
}
pub struct RequestCtx {
pub output_buf: Vec<u8>,
pub request_body: Vec<u8>,
pub body_read_pos: usize,
pub cookie_header: Option<CString>,
pub response_headers: Vec<(String, String)>,
pub status_code: u16,
pub request_headers: Vec<(String, String)>,
pub remote_addr: Option<CString>,
pub server_name: Option<CString>,
pub server_port: u16,
pub https: bool,
pub document_root: Option<String>,
pub c_method: Option<CString>,
pub c_uri: Option<CString>,
pub c_query_string: Option<CString>,
pub c_content_type: Option<CString>,
}
impl RequestCtx {
#[allow(clippy::too_many_arguments)]
pub fn new(
request_body: Vec<u8>,
cookie_header: Option<&str>,
request_headers: Vec<(String, String)>,
remote_addr: Option<&str>,
server_name: Option<&str>,
server_port: u16,
https: bool,
) -> Self {
Self {
output_buf: Vec::with_capacity(8192),
request_body,
body_read_pos: 0,
cookie_header: cookie_header.and_then(|c| {
CString::new(c).ok().or_else(|| {
tracing::warn!("Cookie header contains null bytes — dropped");
None
})
}),
response_headers: Vec::new(),
status_code: 200,
request_headers,
remote_addr: remote_addr.and_then(|s| CString::new(s).ok()),
server_name: server_name.and_then(|s| CString::new(s).ok()),
server_port,
https,
document_root: None,
c_method: None,
c_uri: None,
c_query_string: None,
c_content_type: None,
}
}
pub fn set_request_info(
&mut self,
method: &str,
uri: &str,
query_string: &str,
content_type: Option<&str>,
) {
self.c_method = CString::new(method).ok();
self.c_uri = CString::new(uri).ok();
self.c_query_string = CString::new(query_string).ok();
self.c_content_type = content_type.and_then(|ct| CString::new(ct).ok());
}
pub fn reset_for_worker_request(&mut self, p: WorkerRequestParams<'_>) {
self.output_buf.clear();
self.request_body = p.request_body;
self.body_read_pos = 0;
self.cookie_header = p.cookie_header.and_then(|c| CString::new(c).ok());
self.response_headers.clear();
self.status_code = 200;
self.request_headers = p.request_headers;
self.remote_addr = p.remote_addr.and_then(|s| CString::new(s).ok());
self.server_name = p.server_name.and_then(|s| CString::new(s).ok());
self.server_port = p.server_port;
self.https = p.https;
self.set_request_info(p.method, p.uri, p.query_string, p.content_type);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ctx() -> RequestCtx {
RequestCtx::new(Vec::new(), None, Vec::new(), None, None, 80, false)
}
fn make_ctx_full() -> RequestCtx {
RequestCtx::new(
b"name=test&value=42".to_vec(),
Some("session_id=abc123; theme=dark"),
vec![
("Accept".into(), "text/html".into()),
("User-Agent".into(), "bext-test/1.0".into()),
],
Some("192.168.1.100"),
Some("example.com"),
443,
true,
)
}
#[test]
fn new_empty_context() {
let ctx = make_ctx();
assert!(ctx.output_buf.is_empty());
assert!(ctx.request_body.is_empty());
assert_eq!(ctx.body_read_pos, 0);
assert!(ctx.cookie_header.is_none());
assert!(ctx.response_headers.is_empty());
assert_eq!(ctx.status_code, 200);
assert!(ctx.request_headers.is_empty());
assert!(ctx.remote_addr.is_none());
assert!(ctx.server_name.is_none());
assert_eq!(ctx.server_port, 80);
assert!(!ctx.https);
assert!(ctx.c_method.is_none());
assert!(ctx.c_uri.is_none());
}
#[test]
fn new_full_context() {
let ctx = make_ctx_full();
assert_eq!(ctx.request_body, b"name=test&value=42");
assert!(ctx.cookie_header.is_some());
assert_eq!(ctx.request_headers.len(), 2);
assert_eq!(
ctx.remote_addr.as_ref().unwrap().to_str().unwrap(),
"192.168.1.100"
);
assert_eq!(
ctx.server_name.as_ref().unwrap().to_str().unwrap(),
"example.com"
);
assert_eq!(ctx.server_port, 443);
assert!(ctx.https);
}
#[test]
fn set_request_info() {
let mut ctx = make_ctx();
ctx.set_request_info("POST", "/api/data", "page=1", Some("application/json"));
assert_eq!(ctx.c_method.as_ref().unwrap().to_str().unwrap(), "POST");
assert_eq!(ctx.c_uri.as_ref().unwrap().to_str().unwrap(), "/api/data");
assert_eq!(
ctx.c_query_string.as_ref().unwrap().to_str().unwrap(),
"page=1"
);
assert_eq!(
ctx.c_content_type.as_ref().unwrap().to_str().unwrap(),
"application/json"
);
}
#[test]
fn reset_for_worker_request() {
let mut ctx = make_ctx_full();
ctx.output_buf.extend_from_slice(b"old output");
ctx.response_headers.push(("X-Old".into(), "val".into()));
ctx.reset_for_worker_request(WorkerRequestParams {
request_body: b"new body".to_vec(),
cookie_header: Some("new_cookie=x"),
request_headers: vec![("Accept".into(), "application/json".into())],
method: "PUT",
uri: "/api/update",
query_string: "id=42",
content_type: Some("application/json"),
remote_addr: Some("10.0.0.1"),
server_name: Some("api.example.com"),
server_port: 8080,
https: false,
});
assert!(ctx.output_buf.is_empty());
assert_eq!(ctx.request_body, b"new body");
assert_eq!(ctx.body_read_pos, 0);
assert!(ctx.response_headers.is_empty());
assert_eq!(ctx.status_code, 200);
assert_eq!(ctx.c_method.as_ref().unwrap().to_str().unwrap(), "PUT");
assert_eq!(ctx.server_port, 8080);
assert!(!ctx.https);
}
#[test]
fn output_buf_accumulates() {
let mut ctx = make_ctx();
ctx.output_buf.extend_from_slice(b"<html>");
ctx.output_buf.extend_from_slice(b"<body>Hello</body>");
ctx.output_buf.extend_from_slice(b"</html>");
assert_eq!(
String::from_utf8_lossy(&ctx.output_buf),
"<html><body>Hello</body></html>"
);
}
#[test]
fn cookie_header_null_bytes_filtered() {
let ctx = RequestCtx::new(
Vec::new(),
Some("bad\0cookie"),
Vec::new(),
None,
None,
80,
false,
);
assert!(ctx.cookie_header.is_none());
}
#[test]
fn large_body() {
let body = vec![0x42u8; 1024 * 1024];
let ctx = RequestCtx::new(body, None, Vec::new(), None, None, 80, false);
assert_eq!(ctx.request_body.len(), 1024 * 1024);
}
#[test]
fn many_headers() {
let headers: Vec<(String, String)> = (0..50)
.map(|i| (format!("X-Header-{}", i), format!("value-{}", i)))
.collect();
let ctx = RequestCtx::new(Vec::new(), None, headers, None, None, 80, false);
assert_eq!(ctx.request_headers.len(), 50);
}
}