1use std::io::{BufReader, Read, Write};
22
23pub const DEFAULT_MAX_BODY: usize = 8 * 1024 * 1024;
28
29pub const MAX_HEADER_BYTES: usize = 64 * 1024;
32
33#[derive(Debug, Clone)]
35pub struct Request {
36 pub method: String,
40 pub target: String,
43 pub version: String,
45 pub headers: Vec<(String, String)>,
50 pub body: Vec<u8>,
53}
54
55impl Request {
56 #[must_use]
58 pub fn header(&self, name: &str) -> Option<&str> {
59 self.headers
60 .iter()
61 .find(|(k, _)| k.eq_ignore_ascii_case(name))
62 .map(|(_, v)| v.as_str())
63 }
64
65 #[must_use]
69 pub fn keep_alive(&self) -> bool {
70 let conn = self.header("connection").unwrap_or_default();
71 if conn.eq_ignore_ascii_case("close") {
72 return false;
73 }
74 if self.version == "HTTP/1.0" {
75 return conn.eq_ignore_ascii_case("keep-alive");
76 }
77 true
78 }
79
80 #[must_use]
82 pub fn path(&self) -> &str {
83 self.target.split('?').next().unwrap_or(&self.target)
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct Response {
90 pub status: u16,
92 pub reason: &'static str,
94 pub headers: Vec<(String, String)>,
97 pub content_type: Option<String>,
99 pub body: Vec<u8>,
101}
102
103impl Response {
104 #[must_use]
106 pub fn new(status: u16, reason: &'static str, content_type: &str, body: Vec<u8>) -> Self {
107 Self {
108 status,
109 reason,
110 headers: Vec::new(),
111 content_type: Some(content_type.to_string()),
112 body,
113 }
114 }
115
116 #[must_use]
118 pub fn empty(status: u16, reason: &'static str) -> Self {
119 Self {
120 status,
121 reason,
122 headers: Vec::new(),
123 content_type: None,
124 body: Vec::new(),
125 }
126 }
127
128 #[must_use]
130 pub fn with_header(mut self, name: &str, value: &str) -> Self {
131 self.headers.push((name.to_string(), value.to_string()));
132 self
133 }
134}
135
136#[derive(Debug)]
138pub enum HttpError {
139 ConnectionClosed,
142 Malformed(String),
144 BodyTooLarge,
146 HeaderTooLarge,
149 Io(std::io::Error),
151}
152
153impl std::fmt::Display for HttpError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::ConnectionClosed => f.write_str("connection closed"),
157 Self::Malformed(m) => write!(f, "malformed request: {m}"),
158 Self::BodyTooLarge => f.write_str("request body too large"),
159 Self::HeaderTooLarge => f.write_str("request header block too large"),
160 Self::Io(e) => write!(f, "io: {e}"),
161 }
162 }
163}
164
165impl std::error::Error for HttpError {}
166
167impl From<std::io::Error> for HttpError {
168 fn from(e: std::io::Error) -> Self {
169 Self::Io(e)
170 }
171}
172
173pub fn read_request<R: Read>(
182 stream: &mut BufReader<R>,
183 max_body: usize,
184) -> Result<Request, HttpError> {
185 let head = read_header_block(stream)?;
186 let mut lines = head.split("\r\n");
187 let request_line = lines
188 .next()
189 .ok_or_else(|| HttpError::Malformed("empty request".into()))?;
190 let mut parts = request_line.split(' ');
191 let method = parts
192 .next()
193 .ok_or_else(|| HttpError::Malformed("missing method".into()))?
194 .to_string();
195 let target = parts
196 .next()
197 .ok_or_else(|| HttpError::Malformed("missing target".into()))?
198 .to_string();
199 let version = parts.next().unwrap_or("HTTP/1.1").trim().to_string();
200 if !matches!(version.as_str(), "HTTP/1.0" | "HTTP/1.1") {
201 return Err(HttpError::Malformed(format!(
202 "unsupported version {version}"
203 )));
204 }
205
206 let mut headers = Vec::new();
207 for line in lines {
208 if line.is_empty() {
209 continue;
210 }
211 let (name, value) = line
212 .split_once(':')
213 .ok_or_else(|| HttpError::Malformed(format!("malformed header: {line}")))?;
214 headers.push((name.trim().to_string(), value.trim().to_string()));
215 }
216
217 let req_no_body = Request {
218 method,
219 target,
220 version,
221 headers,
222 body: Vec::new(),
223 };
224
225 let chunked = req_no_body
226 .header("transfer-encoding")
227 .is_some_and(|v| v.eq_ignore_ascii_case("chunked"));
228 let body = if chunked {
229 read_chunked_body(stream, max_body)?
230 } else if let Some(len) = req_no_body.header("content-length") {
231 let len: usize = len
232 .trim()
233 .parse()
234 .map_err(|_| HttpError::Malformed(format!("bad content-length: {len}")))?;
235 if len > max_body {
236 return Err(HttpError::BodyTooLarge);
237 }
238 let mut buf = vec![0u8; len];
239 stream.read_exact(&mut buf)?;
240 buf
241 } else {
242 Vec::new()
243 };
244
245 Ok(Request {
246 body,
247 ..req_no_body
248 })
249}
250
251fn read_header_block<R: Read>(stream: &mut BufReader<R>) -> Result<String, HttpError> {
254 let mut raw = Vec::new();
255 let mut byte = [0u8; 1];
256 loop {
257 let n = stream.read(&mut byte)?;
258 if n == 0 {
259 if raw.is_empty() {
260 return Err(HttpError::ConnectionClosed);
261 }
262 return Err(HttpError::Malformed("connection closed mid-header".into()));
263 }
264 raw.push(byte[0]);
265 if raw.len() > MAX_HEADER_BYTES {
266 return Err(HttpError::HeaderTooLarge);
267 }
268 if raw.ends_with(b"\r\n\r\n") {
269 raw.truncate(raw.len() - 4);
270 break;
271 }
272 }
273 String::from_utf8(raw).map_err(|e| HttpError::Malformed(format!("header not UTF-8: {e}")))
274}
275
276fn read_chunked_body<R: Read>(
278 stream: &mut BufReader<R>,
279 max_body: usize,
280) -> Result<Vec<u8>, HttpError> {
281 let mut body = Vec::new();
282 loop {
283 let size_line = read_line(stream)?;
284 let size_str = size_line.split(';').next().unwrap_or("").trim();
285 let size = usize::from_str_radix(size_str, 16)
286 .map_err(|_| HttpError::Malformed(format!("bad chunk size: {size_str}")))?;
287 if size == 0 {
288 loop {
291 let trailer = read_line(stream)?;
292 if trailer.is_empty() {
293 break;
294 }
295 }
296 break;
297 }
298 if body.len() + size > max_body {
299 return Err(HttpError::BodyTooLarge);
300 }
301 let mut chunk = vec![0u8; size];
302 stream.read_exact(&mut chunk)?;
303 body.extend_from_slice(&chunk);
304 let mut crlf = [0u8; 2];
306 stream.read_exact(&mut crlf)?;
307 if &crlf != b"\r\n" {
308 return Err(HttpError::Malformed("missing chunk CRLF".into()));
309 }
310 }
311 Ok(body)
312}
313
314fn read_line<R: Read>(stream: &mut BufReader<R>) -> Result<String, HttpError> {
315 let mut raw = Vec::new();
316 let mut byte = [0u8; 1];
317 loop {
318 let n = stream.read(&mut byte)?;
319 if n == 0 {
320 return Err(HttpError::Malformed("connection closed mid-chunk".into()));
321 }
322 if byte[0] == b'\n' {
323 if raw.last() == Some(&b'\r') {
324 raw.pop();
325 }
326 break;
327 }
328 raw.push(byte[0]);
329 if raw.len() > MAX_HEADER_BYTES {
330 return Err(HttpError::HeaderTooLarge);
331 }
332 }
333 String::from_utf8(raw).map_err(|e| HttpError::Malformed(format!("chunk line not UTF-8: {e}")))
334}
335
336pub fn write_response<W: Write>(
342 stream: &mut W,
343 response: &Response,
344 keep_alive: bool,
345) -> std::io::Result<()> {
346 let mut out = format!("HTTP/1.1 {} {}\r\n", response.status, response.reason);
347 if let Some(ct) = &response.content_type {
348 out.push_str(&format!("Content-Type: {ct}\r\n"));
349 }
350 out.push_str(&format!("Content-Length: {}\r\n", response.body.len()));
351 out.push_str(&format!(
352 "Connection: {}\r\n",
353 if keep_alive { "keep-alive" } else { "close" }
354 ));
355 for (name, value) in &response.headers {
356 out.push_str(&format!("{name}: {value}\r\n"));
357 }
358 out.push_str("\r\n");
359 stream.write_all(out.as_bytes())?;
360 stream.write_all(&response.body)?;
361 stream.flush()
362}
363
364#[cfg(test)]
365#[allow(clippy::unwrap_used, clippy::expect_used)]
366mod tests {
367 use super::*;
368 use std::io::Cursor;
369
370 fn parse(raw: &[u8]) -> Request {
371 let mut r = BufReader::new(Cursor::new(raw.to_vec()));
372 read_request(&mut r, DEFAULT_MAX_BODY).expect("parses")
373 }
374
375 #[test]
376 fn parses_simple_get_with_no_body() {
377 let req = parse(b"GET /dds/rest1/applications HTTP/1.1\r\nHost: x\r\n\r\n");
378 assert_eq!(req.method, "GET");
379 assert_eq!(req.target, "/dds/rest1/applications");
380 assert!(req.body.is_empty());
381 assert!(req.keep_alive());
382 }
383
384 #[test]
385 fn parses_content_length_body() {
386 let req = parse(b"POST /x HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello");
387 assert_eq!(req.body, b"hello");
388 }
389
390 #[test]
391 fn parses_chunked_body() {
392 let raw = b"POST /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n";
393 let req = parse(raw);
394 assert_eq!(req.body, b"Wikipedia");
395 }
396
397 #[test]
398 fn chunked_body_with_trailer_headers() {
399 let raw = b"POST /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\nX-Trailer: y\r\n\r\n";
400 let req = parse(raw);
401 assert_eq!(req.body, b"abc");
402 }
403
404 #[test]
405 fn content_length_over_cap_is_refused() {
406 let mut r = BufReader::new(Cursor::new(
407 b"POST /x HTTP/1.1\r\nContent-Length: 100\r\n\r\n".to_vec(),
408 ));
409 let err = read_request(&mut r, 10).unwrap_err();
410 assert!(matches!(err, HttpError::BodyTooLarge));
411 }
412
413 #[test]
414 fn chunked_body_over_cap_is_refused() {
415 let raw = b"POST /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n8\r\n01234567\r\n8\r\n89abcdef\r\n0\r\n\r\n";
416 let mut r = BufReader::new(Cursor::new(raw.to_vec()));
417 let err = read_request(&mut r, 10).unwrap_err();
418 assert!(matches!(err, HttpError::BodyTooLarge));
419 }
420
421 #[test]
422 fn empty_stream_is_connection_closed_not_malformed() {
423 let mut r = BufReader::new(Cursor::new(Vec::new()));
424 let err = read_request(&mut r, DEFAULT_MAX_BODY).unwrap_err();
425 assert!(matches!(err, HttpError::ConnectionClosed));
426 }
427
428 #[test]
429 fn http_1_0_without_keep_alive_header_closes() {
430 let req = parse(b"GET /x HTTP/1.0\r\n\r\n");
431 assert!(!req.keep_alive());
432 }
433
434 #[test]
435 fn http_1_0_with_keep_alive_header_stays_open() {
436 let req = parse(b"GET /x HTTP/1.0\r\nConnection: keep-alive\r\n\r\n");
437 assert!(req.keep_alive());
438 }
439
440 #[test]
441 fn http_1_1_with_connection_close_closes() {
442 let req = parse(b"GET /x HTTP/1.1\r\nConnection: close\r\n\r\n");
443 assert!(!req.keep_alive());
444 }
445
446 #[test]
447 fn header_lookup_is_case_insensitive() {
448 let req = parse(b"GET /x HTTP/1.1\r\nContent-Type: text/plain\r\n\r\n");
449 assert_eq!(req.header("content-type"), Some("text/plain"));
450 assert_eq!(req.header("CONTENT-TYPE"), Some("text/plain"));
451 }
452
453 #[test]
454 fn path_strips_query_string() {
455 let req = parse(b"GET /a/b?x=1 HTTP/1.1\r\n\r\n");
456 assert_eq!(req.path(), "/a/b");
457 }
458
459 #[test]
460 fn write_response_round_trip() {
461 let mut out = Vec::new();
462 let resp = Response::new(200, "OK", "text/plain", b"hi".to_vec());
463 write_response(&mut out, &resp, true).unwrap();
464 let text = String::from_utf8(out).unwrap();
465 assert!(text.starts_with("HTTP/1.1 200 OK\r\n"));
466 assert!(text.contains("Content-Length: 2\r\n"));
467 assert!(text.contains("Connection: keep-alive\r\n"));
468 assert!(text.ends_with("\r\n\r\nhi"));
469 }
470
471 #[test]
472 fn write_response_closes_when_not_keep_alive() {
473 let mut out = Vec::new();
474 let resp = Response::empty(204, "No Content");
475 write_response(&mut out, &resp, false).unwrap();
476 let text = String::from_utf8(out).unwrap();
477 assert!(text.contains("Connection: close\r\n"));
478 assert!(text.contains("Content-Length: 0\r\n"));
479 }
480}