1use std::ffi::CString;
7
8pub struct WorkerRequestParams<'a> {
10 pub request_body: Vec<u8>,
11 pub cookie_header: Option<&'a str>,
12 pub request_headers: Vec<(String, String)>,
13 pub method: &'a str,
14 pub uri: &'a str,
15 pub query_string: &'a str,
16 pub content_type: Option<&'a str>,
17 pub remote_addr: Option<&'a str>,
18 pub server_name: Option<&'a str>,
19 pub server_port: u16,
20 pub https: bool,
21}
22
23pub struct RequestCtx {
29 pub output_buf: Vec<u8>,
32
33 pub request_body: Vec<u8>,
36 pub body_read_pos: usize,
38
39 pub cookie_header: Option<CString>,
42 pub response_headers: Vec<(String, String)>,
44 pub status_code: u16,
46 pub request_headers: Vec<(String, String)>,
48
49 pub remote_addr: Option<CString>,
52 pub server_name: Option<CString>,
54 pub server_port: u16,
56 pub https: bool,
58 pub document_root: Option<String>,
60
61 pub c_method: Option<CString>,
64 pub c_uri: Option<CString>,
65 pub c_query_string: Option<CString>,
66 pub c_content_type: Option<CString>,
67}
68
69impl RequestCtx {
70 #[allow(clippy::too_many_arguments)]
72 pub fn new(
73 request_body: Vec<u8>,
74 cookie_header: Option<&str>,
75 request_headers: Vec<(String, String)>,
76 remote_addr: Option<&str>,
77 server_name: Option<&str>,
78 server_port: u16,
79 https: bool,
80 ) -> Self {
81 Self {
82 output_buf: Vec::with_capacity(8192),
83 request_body,
84 body_read_pos: 0,
85 cookie_header: cookie_header.and_then(|c| {
86 CString::new(c).ok().or_else(|| {
87 tracing::warn!("Cookie header contains null bytes — dropped");
88 None
89 })
90 }),
91 response_headers: Vec::new(),
92 status_code: 200,
93 request_headers,
94 remote_addr: remote_addr.and_then(|s| CString::new(s).ok()),
95 server_name: server_name.and_then(|s| CString::new(s).ok()),
96 server_port,
97 https,
98 document_root: None,
99 c_method: None,
100 c_uri: None,
101 c_query_string: None,
102 c_content_type: None,
103 }
104 }
105
106 pub fn set_request_info(
109 &mut self,
110 method: &str,
111 uri: &str,
112 query_string: &str,
113 content_type: Option<&str>,
114 ) {
115 self.c_method = CString::new(method).ok();
116 self.c_uri = CString::new(uri).ok();
117 self.c_query_string = CString::new(query_string).ok();
118 self.c_content_type = content_type.and_then(|ct| CString::new(ct).ok());
119 }
120
121 pub fn reset_for_worker_request(&mut self, p: WorkerRequestParams<'_>) {
124 self.output_buf.clear();
125 self.request_body = p.request_body;
126 self.body_read_pos = 0;
127 self.cookie_header = p.cookie_header.and_then(|c| CString::new(c).ok());
128 self.response_headers.clear();
129 self.status_code = 200;
130 self.request_headers = p.request_headers;
131 self.remote_addr = p.remote_addr.and_then(|s| CString::new(s).ok());
132 self.server_name = p.server_name.and_then(|s| CString::new(s).ok());
133 self.server_port = p.server_port;
134 self.https = p.https;
135 self.set_request_info(p.method, p.uri, p.query_string, p.content_type);
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn make_ctx() -> RequestCtx {
144 RequestCtx::new(Vec::new(), None, Vec::new(), None, None, 80, false)
145 }
146
147 fn make_ctx_full() -> RequestCtx {
148 RequestCtx::new(
149 b"name=test&value=42".to_vec(),
150 Some("session_id=abc123; theme=dark"),
151 vec![
152 ("Accept".into(), "text/html".into()),
153 ("User-Agent".into(), "bext-test/1.0".into()),
154 ],
155 Some("192.168.1.100"),
156 Some("example.com"),
157 443,
158 true,
159 )
160 }
161
162 #[test]
163 fn new_empty_context() {
164 let ctx = make_ctx();
165 assert!(ctx.output_buf.is_empty());
166 assert!(ctx.request_body.is_empty());
167 assert_eq!(ctx.body_read_pos, 0);
168 assert!(ctx.cookie_header.is_none());
169 assert!(ctx.response_headers.is_empty());
170 assert_eq!(ctx.status_code, 200);
171 assert!(ctx.request_headers.is_empty());
172 assert!(ctx.remote_addr.is_none());
173 assert!(ctx.server_name.is_none());
174 assert_eq!(ctx.server_port, 80);
175 assert!(!ctx.https);
176 assert!(ctx.c_method.is_none());
177 assert!(ctx.c_uri.is_none());
178 }
179
180 #[test]
181 fn new_full_context() {
182 let ctx = make_ctx_full();
183 assert_eq!(ctx.request_body, b"name=test&value=42");
184 assert!(ctx.cookie_header.is_some());
185 assert_eq!(ctx.request_headers.len(), 2);
186 assert_eq!(
187 ctx.remote_addr.as_ref().unwrap().to_str().unwrap(),
188 "192.168.1.100"
189 );
190 assert_eq!(
191 ctx.server_name.as_ref().unwrap().to_str().unwrap(),
192 "example.com"
193 );
194 assert_eq!(ctx.server_port, 443);
195 assert!(ctx.https);
196 }
197
198 #[test]
199 fn set_request_info() {
200 let mut ctx = make_ctx();
201 ctx.set_request_info("POST", "/api/data", "page=1", Some("application/json"));
202 assert_eq!(ctx.c_method.as_ref().unwrap().to_str().unwrap(), "POST");
203 assert_eq!(ctx.c_uri.as_ref().unwrap().to_str().unwrap(), "/api/data");
204 assert_eq!(
205 ctx.c_query_string.as_ref().unwrap().to_str().unwrap(),
206 "page=1"
207 );
208 assert_eq!(
209 ctx.c_content_type.as_ref().unwrap().to_str().unwrap(),
210 "application/json"
211 );
212 }
213
214 #[test]
215 fn reset_for_worker_request() {
216 let mut ctx = make_ctx_full();
217 ctx.output_buf.extend_from_slice(b"old output");
218 ctx.response_headers.push(("X-Old".into(), "val".into()));
219
220 ctx.reset_for_worker_request(WorkerRequestParams {
221 request_body: b"new body".to_vec(),
222 cookie_header: Some("new_cookie=x"),
223 request_headers: vec![("Accept".into(), "application/json".into())],
224 method: "PUT",
225 uri: "/api/update",
226 query_string: "id=42",
227 content_type: Some("application/json"),
228 remote_addr: Some("10.0.0.1"),
229 server_name: Some("api.example.com"),
230 server_port: 8080,
231 https: false,
232 });
233
234 assert!(ctx.output_buf.is_empty());
235 assert_eq!(ctx.request_body, b"new body");
236 assert_eq!(ctx.body_read_pos, 0);
237 assert!(ctx.response_headers.is_empty());
238 assert_eq!(ctx.status_code, 200);
239 assert_eq!(ctx.c_method.as_ref().unwrap().to_str().unwrap(), "PUT");
240 assert_eq!(ctx.server_port, 8080);
241 assert!(!ctx.https);
242 }
243
244 #[test]
245 fn output_buf_accumulates() {
246 let mut ctx = make_ctx();
247 ctx.output_buf.extend_from_slice(b"<html>");
248 ctx.output_buf.extend_from_slice(b"<body>Hello</body>");
249 ctx.output_buf.extend_from_slice(b"</html>");
250 assert_eq!(
251 String::from_utf8_lossy(&ctx.output_buf),
252 "<html><body>Hello</body></html>"
253 );
254 }
255
256 #[test]
257 fn cookie_header_null_bytes_filtered() {
258 let ctx = RequestCtx::new(
259 Vec::new(),
260 Some("bad\0cookie"),
261 Vec::new(),
262 None,
263 None,
264 80,
265 false,
266 );
267 assert!(ctx.cookie_header.is_none());
268 }
269
270 #[test]
271 fn large_body() {
272 let body = vec![0x42u8; 1024 * 1024];
273 let ctx = RequestCtx::new(body, None, Vec::new(), None, None, 80, false);
274 assert_eq!(ctx.request_body.len(), 1024 * 1024);
275 }
276
277 #[test]
278 fn many_headers() {
279 let headers: Vec<(String, String)> = (0..50)
280 .map(|i| (format!("X-Header-{}", i), format!("value-{}", i)))
281 .collect();
282 let ctx = RequestCtx::new(Vec::new(), None, headers, None, None, 80, false);
283 assert_eq!(ctx.request_headers.len(), 50);
284 }
285}