1use std::fmt::{self, Display, Formatter};
2use std::io::{BufRead, BufReader, Read};
3use std::str;
4
5use crate::auth::{parse_authenticate_headers, Challenge};
6use crate::errors::NanoGetError;
7use crate::request::{Header, Method};
8
9const READ_CHUNK_SIZE: usize = 8 * 1024;
10const MAX_HEADER_COUNT: usize = 1024;
11const MAX_LINE_BYTES: usize = 16 * 1024;
12const SMALL_BODY_FAST_PATH_LIMIT: usize = 64 * 1024;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum HttpVersion {
17 Http10,
19 Http11,
21}
22
23impl Display for HttpVersion {
24 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Http10 => write!(f, "HTTP/1.0"),
27 Self::Http11 => write!(f, "HTTP/1.1"),
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Response {
35 pub version: HttpVersion,
37 pub status_code: u16,
39 pub reason_phrase: String,
41 pub headers: Vec<Header>,
43 pub trailers: Vec<Header>,
45 pub body: Vec<u8>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) struct ResponseHead {
51 pub version: HttpVersion,
52 pub status_code: u16,
53 pub reason_phrase: String,
54 pub headers: Vec<Header>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum BodyKind {
59 None,
60 ContentLength,
61 Chunked,
62 CloseDelimited,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub(crate) struct ParsedResponse {
67 pub response: Response,
68 pub body_kind: BodyKind,
69 pub connection_close: bool,
70}
71
72impl Response {
73 pub fn header(&self, name: &str) -> Option<&str> {
75 self.headers
76 .iter()
77 .find(|header| header.matches_name(name))
78 .map(Header::value)
79 }
80
81 pub fn headers_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Header> + 'a {
83 self.headers
84 .iter()
85 .filter(move |header| header.matches_name(name))
86 }
87
88 pub fn trailer(&self, name: &str) -> Option<&str> {
90 self.trailers
91 .iter()
92 .find(|header| header.matches_name(name))
93 .map(Header::value)
94 }
95
96 pub fn www_authenticate_challenges(&self) -> Result<Vec<Challenge>, NanoGetError> {
98 parse_authenticate_headers(&self.headers, "www-authenticate")
99 }
100
101 pub fn proxy_authenticate_challenges(&self) -> Result<Vec<Challenge>, NanoGetError> {
103 parse_authenticate_headers(&self.headers, "proxy-authenticate")
104 }
105
106 pub fn body_text(&self) -> Result<&str, NanoGetError> {
108 Ok(str::from_utf8(&self.body)?)
109 }
110
111 pub fn into_body_text(self) -> Result<String, NanoGetError> {
113 String::from_utf8(self.body).map_err(|error| NanoGetError::InvalidUtf8(error.utf8_error()))
114 }
115
116 pub fn is_success(&self) -> bool {
118 (200..=299).contains(&self.status_code)
119 }
120
121 pub fn is_redirection(&self) -> bool {
123 (300..=399).contains(&self.status_code)
124 }
125
126 pub fn is_client_error(&self) -> bool {
128 (400..=499).contains(&self.status_code)
129 }
130
131 pub fn is_server_error(&self) -> bool {
133 (500..=599).contains(&self.status_code)
134 }
135}
136
137#[cfg(test)]
138pub(crate) fn read_response<R: Read>(
139 reader: &mut BufReader<R>,
140 method: Method,
141) -> Result<Response, NanoGetError> {
142 Ok(read_parsed_response(reader, method, true)?.response)
143}
144
145pub(crate) fn read_parsed_response<R: Read>(
146 reader: &mut BufReader<R>,
147 method: Method,
148 strict: bool,
149) -> Result<ParsedResponse, NanoGetError> {
150 loop {
151 let head = read_response_head(reader, strict)?;
152
153 if (100..=199).contains(&head.status_code) && head.status_code != 101 {
154 continue;
155 }
156
157 let body_kind = determine_body_kind(&head.headers, method, head.status_code, strict)?;
158 let (body, trailers) = match body_kind {
159 BodyKind::None => (Vec::new(), Vec::new()),
160 BodyKind::Chunked => read_chunked_body(reader, strict)?,
161 BodyKind::ContentLength => {
162 let content_length = content_length(&head.headers)?.unwrap_or(0);
163 read_content_length_body(reader, content_length)?
164 }
165 BodyKind::CloseDelimited => read_eof_body(reader)?,
166 };
167
168 let connection_close = should_close_connection(head.version, &head.headers, body_kind);
169
170 return Ok(ParsedResponse {
171 response: Response {
172 version: head.version,
173 status_code: head.status_code,
174 reason_phrase: head.reason_phrase,
175 headers: head.headers,
176 trailers,
177 body,
178 },
179 body_kind,
180 connection_close,
181 });
182 }
183}
184
185pub(crate) fn read_response_head<R: BufRead>(
186 reader: &mut R,
187 strict: bool,
188) -> Result<ResponseHead, NanoGetError> {
189 let (status_line, status_line_has_crlf) = read_line(reader).map_err(|error| match error {
190 NanoGetError::Io(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof => {
191 NanoGetError::IncompleteMessage("unexpected EOF while reading status line".to_string())
192 }
193 NanoGetError::Io(error) => NanoGetError::MalformedStatusLine(error.to_string()),
194 NanoGetError::MalformedHeader(line) => NanoGetError::MalformedStatusLine(line),
195 other => other,
196 })?;
197 if strict && !status_line_has_crlf {
198 return Err(NanoGetError::MalformedStatusLine(status_line));
199 }
200 let (version, status_code, reason_phrase) = parse_status_line(&status_line)?;
201 let headers = read_headers(reader, strict)?;
202 Ok(ResponseHead {
203 version,
204 status_code,
205 reason_phrase,
206 headers,
207 })
208}
209
210fn read_headers<R: BufRead>(reader: &mut R, strict: bool) -> Result<Vec<Header>, NanoGetError> {
211 let mut headers = Vec::new();
212
213 loop {
214 let (line, has_crlf) = read_line(reader).map_err(|error| match error {
215 NanoGetError::Io(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof => {
216 NanoGetError::IncompleteMessage(
217 "unexpected EOF while reading header section".to_string(),
218 )
219 }
220 NanoGetError::Io(io_error) => NanoGetError::MalformedHeader(io_error.to_string()),
221 other => other,
222 })?;
223 if strict && !has_crlf {
224 return Err(NanoGetError::MalformedHeader(line));
225 }
226
227 if line.is_empty() {
228 return Ok(headers);
229 }
230
231 if line.starts_with(' ') || line.starts_with('\t') {
232 return Err(NanoGetError::MalformedHeader(line));
233 }
234
235 let (name, value) = line
236 .split_once(':')
237 .ok_or_else(|| NanoGetError::MalformedHeader(line.clone()))?;
238 if headers.len() >= MAX_HEADER_COUNT {
239 return Err(NanoGetError::MalformedHeader(
240 "too many response headers".to_string(),
241 ));
242 }
243 headers.push(Header::new(name.to_string(), value.trim().to_string())?);
244 }
245}
246
247fn read_line<R: BufRead>(reader: &mut R) -> Result<(String, bool), NanoGetError> {
248 let mut line = Vec::new();
249 let mut limited = reader.take((MAX_LINE_BYTES + 1) as u64);
250 let bytes_read = limited.read_until(b'\n', &mut line)?;
251 if bytes_read == 0 {
252 return Err(NanoGetError::Io(std::io::Error::new(
253 std::io::ErrorKind::UnexpectedEof,
254 "unexpected EOF",
255 )));
256 }
257 if line.len() > MAX_LINE_BYTES {
258 return Err(NanoGetError::MalformedHeader(
259 "line exceeds maximum length".to_string(),
260 ));
261 }
262
263 let mut has_crlf = false;
264 if line.ends_with(b"\r\n") {
265 has_crlf = true;
266 line.truncate(line.len() - 2);
267 } else if line.ends_with(b"\n") {
268 line.truncate(line.len() - 1);
269 }
270
271 let text = String::from_utf8(line)
272 .map_err(|error| NanoGetError::MalformedHeader(error.utf8_error().to_string()))?;
273 Ok((text, has_crlf))
274}
275
276fn parse_status_line(line: &str) -> Result<(HttpVersion, u16, String), NanoGetError> {
277 let mut parts = line.splitn(3, ' ');
278 let version = match parts.next() {
279 Some("HTTP/1.0") => HttpVersion::Http10,
280 Some("HTTP/1.1") => HttpVersion::Http11,
281 _ => return Err(NanoGetError::MalformedStatusLine(line.to_string())),
282 };
283
284 let status_code_token = parts
285 .next()
286 .ok_or_else(|| NanoGetError::MalformedStatusLine(line.to_string()))?;
287 if status_code_token.len() != 3 || !status_code_token.bytes().all(|byte| byte.is_ascii_digit())
288 {
289 return Err(NanoGetError::MalformedStatusLine(line.to_string()));
290 }
291
292 let status_code = status_code_token
293 .parse::<u16>()
294 .map_err(|_| NanoGetError::MalformedStatusLine(line.to_string()))?;
295
296 let reason_phrase = parts.next().unwrap_or("").to_string();
297 Ok((version, status_code, reason_phrase))
298}
299
300fn determine_body_kind(
301 headers: &[Header],
302 method: Method,
303 status_code: u16,
304 strict: bool,
305) -> Result<BodyKind, NanoGetError> {
306 if response_has_no_body(method, status_code) {
307 return Ok(BodyKind::None);
308 }
309
310 let has_content_length = content_length(headers)?.is_some();
311 if let Some(transfer_encoding) = transfer_encoding(headers)? {
312 if strict && has_content_length {
313 return Err(NanoGetError::AmbiguousResponseFraming(
314 "response contains both Transfer-Encoding and Content-Length".to_string(),
315 ));
316 }
317 if transfer_encoding.eq_ignore_ascii_case("chunked") {
318 return Ok(BodyKind::Chunked);
319 }
320
321 return Err(NanoGetError::UnsupportedTransferEncoding(transfer_encoding));
322 }
323
324 if has_content_length {
325 return Ok(BodyKind::ContentLength);
326 }
327
328 Ok(BodyKind::CloseDelimited)
329}
330
331fn response_has_no_body(method: Method, status_code: u16) -> bool {
332 method == Method::Head
333 || (100..=199).contains(&status_code)
334 || status_code == 204
335 || status_code == 304
336}
337
338fn transfer_encoding(headers: &[Header]) -> Result<Option<String>, NanoGetError> {
339 let values: Vec<&str> = headers
340 .iter()
341 .filter(|header| header.matches_name("transfer-encoding"))
342 .map(Header::value)
343 .collect();
344
345 if values.is_empty() {
346 return Ok(None);
347 }
348
349 let tokens: Vec<String> = values
350 .iter()
351 .flat_map(|value| value.split(','))
352 .map(str::trim)
353 .filter(|value| !value.is_empty())
354 .map(str::to_string)
355 .collect();
356
357 if tokens.len() == 1 {
358 return Ok(Some(tokens[0].clone()));
359 }
360
361 Err(NanoGetError::UnsupportedTransferEncoding(tokens.join(",")))
362}
363
364pub(crate) fn content_length(headers: &[Header]) -> Result<Option<usize>, NanoGetError> {
365 let mut values = headers
366 .iter()
367 .filter(|header| header.matches_name("content-length"))
368 .flat_map(|header| header.value().split(','))
369 .map(str::trim)
370 .filter(|value| !value.is_empty())
371 .map(parse_content_length_value);
372
373 let Some(first) = values.next() else {
374 return Ok(None);
375 };
376 let first = first?;
377
378 for value in values {
379 if value? != first {
380 return Err(NanoGetError::InvalidContentLength(
381 "mismatched duplicate content-length headers".to_string(),
382 ));
383 }
384 }
385
386 Ok(Some(first))
387}
388
389fn parse_content_length_value(value: &str) -> Result<usize, NanoGetError> {
390 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
391 return Err(NanoGetError::InvalidContentLength(value.to_string()));
392 }
393
394 value
395 .parse::<usize>()
396 .map_err(|_| NanoGetError::InvalidContentLength(value.to_string()))
397}
398
399fn read_content_length_body<R: Read>(
400 reader: &mut R,
401 content_length: usize,
402) -> Result<(Vec<u8>, Vec<Header>), NanoGetError> {
403 if content_length <= SMALL_BODY_FAST_PATH_LIMIT {
404 let mut body = vec![0u8; content_length];
405 reader.read_exact(&mut body).map_err(|error| {
406 if error.kind() == std::io::ErrorKind::UnexpectedEof {
407 NanoGetError::IncompleteMessage(
408 "unexpected EOF while reading Content-Length body".to_string(),
409 )
410 } else {
411 NanoGetError::Io(error)
412 }
413 })?;
414 return Ok((body, Vec::new()));
415 }
416
417 let mut body = Vec::with_capacity(SMALL_BODY_FAST_PATH_LIMIT);
418 read_exact_into_vec(
419 reader,
420 &mut body,
421 content_length,
422 "unexpected EOF while reading Content-Length body",
423 )?;
424 Ok((body, Vec::new()))
425}
426
427fn read_eof_body<R: Read>(reader: &mut R) -> Result<(Vec<u8>, Vec<Header>), NanoGetError> {
428 let mut body = Vec::new();
429 reader.read_to_end(&mut body)?;
430 Ok((body, Vec::new()))
431}
432
433fn read_chunked_body<R: BufRead>(
434 reader: &mut R,
435 strict: bool,
436) -> Result<(Vec<u8>, Vec<Header>), NanoGetError> {
437 let mut body = Vec::new();
438
439 loop {
440 let (line, has_crlf) = read_line(reader).map_err(|error| match error {
441 NanoGetError::Io(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof => {
442 NanoGetError::IncompleteMessage(
443 "unexpected EOF while reading chunk size".to_string(),
444 )
445 }
446 NanoGetError::Io(io_error) => NanoGetError::InvalidChunk(io_error.to_string()),
447 other => other,
448 })?;
449 if strict && !has_crlf {
450 return Err(NanoGetError::InvalidChunk(
451 "chunk-size line is not CRLF-terminated".to_string(),
452 ));
453 }
454 let size_token = line.split(';').next().unwrap_or("").trim();
455 let chunk_size = usize::from_str_radix(size_token, 16)
456 .map_err(|_| NanoGetError::InvalidChunk(line.clone()))?;
457
458 if chunk_size == 0 {
459 let trailers = read_headers(reader, strict)?;
460 return Ok((body, trailers));
461 }
462
463 if body.len().checked_add(chunk_size).is_none() {
464 return Err(NanoGetError::InvalidChunk(
465 "chunk-size overflow while assembling body".to_string(),
466 ));
467 }
468 if chunk_size <= SMALL_BODY_FAST_PATH_LIMIT {
469 let start = body.len();
470 body.resize(start + chunk_size, 0);
471 if let Err(error) = reader.read_exact(&mut body[start..]) {
472 body.truncate(start);
473 if error.kind() == std::io::ErrorKind::UnexpectedEof {
474 return Err(NanoGetError::IncompleteMessage(
475 "unexpected EOF while reading chunk body".to_string(),
476 ));
477 }
478 return Err(NanoGetError::Io(error));
479 }
480 } else {
481 read_exact_into_vec(
482 reader,
483 &mut body,
484 chunk_size,
485 "unexpected EOF while reading chunk body",
486 )?;
487 }
488
489 let mut crlf = [0u8; 2];
490 reader.read_exact(&mut crlf).map_err(|error| {
491 if error.kind() == std::io::ErrorKind::UnexpectedEof {
492 NanoGetError::IncompleteMessage("unexpected EOF after chunk body".to_string())
493 } else {
494 NanoGetError::Io(error)
495 }
496 })?;
497 if crlf != *b"\r\n" {
498 return Err(NanoGetError::InvalidChunk(
499 "missing CRLF after chunk body".to_string(),
500 ));
501 }
502 }
503}
504
505fn read_exact_into_vec<R: Read>(
506 reader: &mut R,
507 body: &mut Vec<u8>,
508 mut remaining: usize,
509 eof_message: &str,
510) -> Result<(), NanoGetError> {
511 let mut chunk = [0u8; READ_CHUNK_SIZE];
512 while remaining > 0 {
513 let to_read = remaining.min(chunk.len());
514 match reader.read(&mut chunk[..to_read]) {
515 Ok(0) => {
516 return Err(NanoGetError::IncompleteMessage(eof_message.to_string()));
517 }
518 Ok(read) => {
519 body.extend_from_slice(&chunk[..read]);
520 remaining -= read;
521 }
522 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
523 return Err(NanoGetError::IncompleteMessage(eof_message.to_string()));
524 }
525 Err(error) => return Err(NanoGetError::Io(error)),
526 }
527 }
528 Ok(())
529}
530
531pub(crate) fn should_close_connection(
532 version: HttpVersion,
533 headers: &[Header],
534 body_kind: BodyKind,
535) -> bool {
536 if body_kind == BodyKind::CloseDelimited {
537 return true;
538 }
539
540 if has_connection_token(headers, "close") {
541 return true;
542 }
543
544 version == HttpVersion::Http10 && !has_connection_token(headers, "keep-alive")
545}
546
547fn has_connection_token(headers: &[Header], token: &str) -> bool {
548 headers
549 .iter()
550 .filter(|header| header.matches_name("connection"))
551 .flat_map(|header| header.value().split(','))
552 .map(str::trim)
553 .any(|candidate| candidate.eq_ignore_ascii_case(token))
554}
555
556#[cfg(test)]
557pub(crate) fn parse_response_bytes(bytes: &[u8], method: Method) -> Result<Response, NanoGetError> {
558 let mut reader = BufReader::new(bytes);
559 read_response(&mut reader, method)
560}
561
562#[cfg(test)]
563mod tests {
564 use std::io::{self, BufRead, BufReader, Cursor, Read};
565 use std::panic::{self, AssertUnwindSafe};
566
567 use super::{
568 parse_response_bytes, read_chunked_body, read_content_length_body, read_parsed_response,
569 read_response_head, BodyKind, HttpVersion,
570 };
571 use crate::errors::NanoGetError;
572 use crate::request::Method;
573
574 #[test]
575 fn parses_content_length_response() {
576 let response = parse_response_bytes(
577 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nX-Test: 1\r\n\r\nhello",
578 Method::Get,
579 )
580 .unwrap();
581 assert_eq!(response.version, HttpVersion::Http11);
582 assert_eq!(response.status_code, 200);
583 assert_eq!(response.reason_phrase, "OK");
584 assert_eq!(response.header("x-test"), Some("1"));
585 assert_eq!(response.body, b"hello");
586 }
587
588 #[test]
589 fn parses_reason_phrases_with_spaces() {
590 let response = parse_response_bytes(
591 b"HTTP/1.0 404 Not Found Here\r\nContent-Length: 0\r\n\r\n",
592 Method::Get,
593 )
594 .unwrap();
595 assert_eq!(response.version, HttpVersion::Http10);
596 assert_eq!(response.reason_phrase, "Not Found Here");
597 }
598
599 #[test]
600 fn parses_chunked_responses_and_trailers() {
601 let response = parse_response_bytes(
602 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nrust\r\n6\r\nacean!\r\n0\r\nX-Trailer: done\r\n\r\n",
603 Method::Get,
604 )
605 .unwrap();
606 assert_eq!(response.body, b"rustacean!");
607 assert_eq!(response.trailer("x-trailer"), Some("done"));
608 }
609
610 #[test]
611 fn head_responses_ignore_declared_body() {
612 let response = parse_response_bytes(
613 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
614 Method::Head,
615 )
616 .unwrap();
617 assert!(response.body.is_empty());
618 }
619
620 #[test]
621 fn parses_connection_close_bodies() {
622 let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\r\n\r\neof body"[..]);
623 let parsed = read_parsed_response(&mut reader, Method::Get, true).unwrap();
624 assert_eq!(parsed.body_kind, BodyKind::CloseDelimited);
625 assert!(parsed.connection_close);
626 assert_eq!(parsed.response.body, b"eof body");
627 }
628
629 #[test]
630 fn rejects_invalid_status_lines() {
631 let error = parse_response_bytes(b"HTP/1.1 200 OK\r\n\r\n", Method::Get).unwrap_err();
632 assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
633
634 let error = parse_response_bytes(b"HTTP/1.1 20 OK\r\n\r\n", Method::Get).unwrap_err();
635 assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
636
637 let error = parse_response_bytes(b"HTTP/1.1 2000 OK\r\n\r\n", Method::Get).unwrap_err();
638 assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
639 }
640
641 #[test]
642 fn rejects_unsupported_transfer_encodings() {
643 let error = parse_response_bytes(
644 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\n\r\n",
645 Method::Get,
646 )
647 .unwrap_err();
648 assert!(matches!(
649 error,
650 NanoGetError::UnsupportedTransferEncoding(_)
651 ));
652
653 let error = parse_response_bytes(
654 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip, chunked\r\n\r\n",
655 Method::Get,
656 )
657 .unwrap_err();
658 assert!(matches!(
659 error,
660 NanoGetError::UnsupportedTransferEncoding(_)
661 ));
662 }
663
664 #[test]
665 fn rejects_mismatched_duplicate_content_lengths() {
666 let error = parse_response_bytes(
667 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello!",
668 Method::Get,
669 )
670 .unwrap_err();
671 assert!(matches!(error, NanoGetError::InvalidContentLength(_)));
672 }
673
674 #[test]
675 fn accepts_duplicate_content_lengths_with_equal_numeric_values() {
676 let response = parse_response_bytes(
677 b"HTTP/1.1 200 OK\r\nContent-Length: 05\r\nContent-Length: 5\r\n\r\nhello",
678 Method::Get,
679 )
680 .unwrap();
681 assert_eq!(response.body, b"hello");
682 }
683
684 #[test]
685 fn rejects_non_numeric_content_lengths() {
686 let error = parse_response_bytes(
687 b"HTTP/1.1 200 OK\r\nContent-Length: +5\r\n\r\nhello",
688 Method::Get,
689 )
690 .unwrap_err();
691 assert!(matches!(error, NanoGetError::InvalidContentLength(_)));
692 }
693
694 #[test]
695 fn rejects_invalid_chunk_sizes() {
696 let error = parse_response_bytes(
697 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nbogus\r\n",
698 Method::Get,
699 )
700 .unwrap_err();
701 assert!(matches!(error, NanoGetError::InvalidChunk(_)));
702 }
703
704 #[test]
705 fn body_text_reports_invalid_utf8() {
706 let response = parse_response_bytes(
707 b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n\xff\xff",
708 Method::Get,
709 )
710 .unwrap();
711 assert!(matches!(
712 response.body_text(),
713 Err(NanoGetError::InvalidUtf8(_))
714 ));
715 }
716
717 #[test]
718 fn skips_interim_responses_but_not_switching_protocols() {
719 let response = parse_response_bytes(
720 b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok",
721 Method::Get,
722 )
723 .unwrap();
724 assert_eq!(response.status_code, 200);
725 assert_eq!(response.body, b"ok");
726
727 let response =
728 parse_response_bytes(b"HTTP/1.1 101 Switching Protocols\r\n\r\n", Method::Get).unwrap();
729 assert_eq!(response.status_code, 101);
730 }
731
732 #[test]
733 fn rejects_malformed_headers() {
734 let error = parse_response_bytes(b"HTTP/1.1 200 OK\r\nBroken-Header\r\n\r\n", Method::Get)
735 .unwrap_err();
736 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
737 }
738
739 #[test]
740 fn preserves_duplicate_headers() {
741 let response = parse_response_bytes(
742 b"HTTP/1.1 200 OK\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\nContent-Length: 0\r\n\r\n",
743 Method::Get,
744 )
745 .unwrap();
746 let cookies: Vec<_> = response
747 .headers_named("set-cookie")
748 .map(|header| header.value().to_string())
749 .collect();
750 assert_eq!(cookies, vec!["a=1".to_string(), "b=2".to_string()]);
751 }
752
753 #[test]
754 fn parses_response_heads() {
755 let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\r\nX-Test: yes\r\n\r\n"[..]);
756 let head = read_response_head(&mut reader, true).unwrap();
757 assert_eq!(head.status_code, 200);
758 assert_eq!(head.headers[0].value(), "yes");
759 }
760
761 #[test]
762 fn keep_alive_is_honored_for_http_10() {
763 let mut reader = BufReader::new(
764 &b"HTTP/1.0 200 OK\r\nConnection: keep-alive\r\nContent-Length: 2\r\n\r\nok"[..],
765 );
766 let parsed = read_parsed_response(&mut reader, Method::Get, true).unwrap();
767 assert!(!parsed.connection_close);
768 }
769
770 #[test]
771 fn strict_mode_rejects_lf_only_lines() {
772 let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\nContent-Length: 0\n\n"[..]);
773 let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
774 assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
775 }
776
777 #[test]
778 fn lenient_mode_accepts_lf_only_lines() {
779 let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\nContent-Length: 2\n\nok"[..]);
780 let parsed = read_parsed_response(&mut reader, Method::Get, false).unwrap();
781 assert_eq!(parsed.response.body, b"ok");
782 }
783
784 #[test]
785 fn strict_mode_rejects_transfer_encoding_with_content_length() {
786 let mut reader = BufReader::new(
787 &b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 2\r\n\r\n2\r\nok\r\n0\r\n\r\n"[..],
788 );
789 let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
790 assert!(matches!(error, NanoGetError::AmbiguousResponseFraming(_)));
791 }
792
793 #[test]
794 fn incomplete_content_length_body_reports_incomplete_message() {
795 let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nok"[..]);
796 let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
797 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
798 }
799
800 #[test]
801 fn incomplete_chunked_body_reports_incomplete_message() {
802 let mut reader =
803 BufReader::new(&b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nok"[..]);
804 let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
805 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
806 }
807
808 #[test]
809 fn response_status_helpers_cover_all_classes() {
810 let ok = parse_response_bytes(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", Method::Get)
811 .unwrap();
812 assert!(ok.is_success());
813 assert!(!ok.is_redirection());
814
815 let redirect = parse_response_bytes(
816 b"HTTP/1.1 302 Found\r\nContent-Length: 0\r\n\r\n",
817 Method::Get,
818 )
819 .unwrap();
820 assert!(redirect.is_redirection());
821
822 let client_error = parse_response_bytes(
823 b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n",
824 Method::Get,
825 )
826 .unwrap();
827 assert!(client_error.is_client_error());
828
829 let server_error = parse_response_bytes(
830 b"HTTP/1.1 500 Boom\r\nContent-Length: 0\r\n\r\n",
831 Method::Get,
832 )
833 .unwrap();
834 assert!(server_error.is_server_error());
835 }
836
837 #[test]
838 fn parses_authenticate_challenges_from_response_helpers() {
839 let response = parse_response_bytes(
840 b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"api\"\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
841 Method::Get,
842 )
843 .unwrap();
844 assert_eq!(response.www_authenticate_challenges().unwrap().len(), 1);
845 assert_eq!(response.proxy_authenticate_challenges().unwrap().len(), 1);
846 }
847
848 #[test]
849 fn http_version_display_formats_wire_tokens() {
850 assert_eq!(HttpVersion::Http10.to_string(), "HTTP/1.0");
851 assert_eq!(HttpVersion::Http11.to_string(), "HTTP/1.1");
852 }
853
854 #[test]
855 fn response_head_maps_status_and_header_read_io_errors() {
856 let mut empty = BufReader::new(&b""[..]);
857 let error = read_response_head(&mut empty, true).unwrap_err();
858 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
859
860 let mut truncated = BufReader::new(&b"HTTP/1.1 200 OK\r\nX-Test: 1\r\n"[..]);
861 let error = read_response_head(&mut truncated, true).unwrap_err();
862 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
863
864 let mut invalid_status = BufReader::new(&b"\xff\r\n\r\n"[..]);
865 let error = read_response_head(&mut invalid_status, true).unwrap_err();
866 assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
867
868 let mut invalid_header = BufReader::new(&b"HTTP/1.1 200 OK\r\nX:\xff\r\n\r\n"[..]);
869 let error = read_response_head(&mut invalid_header, true).unwrap_err();
870 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
871 }
872
873 #[test]
874 fn strict_header_parsing_rejects_lf_only_and_obs_fold() {
875 let mut lf_only = BufReader::new(&b"HTTP/1.1 200 OK\r\nX-Test: 1\n\n"[..]);
876 let error = read_response_head(&mut lf_only, true).unwrap_err();
877 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
878
879 let mut obs_fold = BufReader::new(&b"HTTP/1.1 200 OK\r\n value\r\n\r\n"[..]);
880 let error = read_response_head(&mut obs_fold, true).unwrap_err();
881 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
882 }
883
884 struct FailingRead {
885 cursor: Cursor<Vec<u8>>,
886 fail_at: usize,
887 kind: io::ErrorKind,
888 }
889
890 impl Read for FailingRead {
891 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
892 let pos = self.cursor.position() as usize;
893 if pos >= self.fail_at {
894 return Err(io::Error::new(self.kind, "forced read failure"));
895 }
896 let max = (self.fail_at - pos).min(buf.len());
897 self.cursor.read(&mut buf[..max])
898 }
899 }
900
901 struct AlwaysErrBufRead;
902
903 impl Read for AlwaysErrBufRead {
904 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
905 Err(io::Error::new(io::ErrorKind::Other, "forced read failure"))
906 }
907 }
908
909 impl BufRead for AlwaysErrBufRead {
910 fn fill_buf(&mut self) -> io::Result<&[u8]> {
911 Err(io::Error::new(io::ErrorKind::Other, "forced fill failure"))
912 }
913
914 fn consume(&mut self, _amt: usize) {}
915 }
916
917 struct DeterministicRng {
918 state: u64,
919 }
920
921 impl DeterministicRng {
922 fn new(seed: u64) -> Self {
923 Self { state: seed }
924 }
925
926 fn next_u32(&mut self) -> u32 {
927 self.state = self
928 .state
929 .wrapping_mul(6_364_136_223_846_793_005)
930 .wrapping_add(1);
931 (self.state >> 32) as u32
932 }
933
934 fn next_bytes(&mut self, max_len: usize) -> Vec<u8> {
935 let len = (self.next_u32() as usize) % (max_len + 1);
936 let mut bytes = Vec::with_capacity(len);
937 for _ in 0..len {
938 bytes.push((self.next_u32() & 0xff) as u8);
939 }
940 bytes
941 }
942 }
943
944 #[test]
945 fn body_readers_map_non_eof_io_failures() {
946 let mut failing = FailingRead {
947 cursor: Cursor::new(vec![1, 2, 3]),
948 fail_at: 0,
949 kind: io::ErrorKind::Other,
950 };
951 let error = read_content_length_body(&mut failing, 1).unwrap_err();
952 assert!(matches!(error, NanoGetError::Io(_)));
953
954 let mut failing_chunk = AlwaysErrBufRead;
955 let error = read_chunked_body(&mut failing_chunk, true).unwrap_err();
956 assert!(matches!(error, NanoGetError::InvalidChunk(_)));
957
958 let mut read_buf = [0u8; 1];
959 let mut always = AlwaysErrBufRead;
960 assert!(always.read(&mut read_buf).is_err());
961 always.consume(0);
962 }
963
964 #[test]
965 fn chunked_parser_covers_additional_error_paths() {
966 let mut empty = BufReader::new(&b""[..]);
967 let error = read_chunked_body(&mut empty, true).unwrap_err();
968 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
969
970 let mut lf_only = BufReader::new(&b"2\nok\r\n0\r\n\r\n"[..]);
971 let error = read_chunked_body(&mut lf_only, true).unwrap_err();
972 assert!(matches!(error, NanoGetError::InvalidChunk(_)));
973
974 let mut body_fail = BufReader::new(FailingRead {
975 cursor: Cursor::new(b"2\r\nok\r\n0\r\n\r\n".to_vec()),
976 fail_at: 3,
977 kind: io::ErrorKind::Other,
978 });
979 let error = read_chunked_body(&mut body_fail, true).unwrap_err();
980 assert!(matches!(error, NanoGetError::Io(_)));
981
982 let mut crlf_eof = BufReader::new(&b"2\r\nok"[..]);
983 let error = read_chunked_body(&mut crlf_eof, true).unwrap_err();
984 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
985
986 let mut crlf_fail = BufReader::new(FailingRead {
987 cursor: Cursor::new(b"2\r\nok\r\n0\r\n\r\n".to_vec()),
988 fail_at: 5,
989 kind: io::ErrorKind::Other,
990 });
991 let error = read_chunked_body(&mut crlf_fail, true).unwrap_err();
992 assert!(matches!(error, NanoGetError::Io(_)));
993
994 let mut bad_crlf = BufReader::new(&b"2\r\nokxx"[..]);
995 let error = read_chunked_body(&mut bad_crlf, true).unwrap_err();
996 assert!(matches!(error, NanoGetError::InvalidChunk(_)));
997
998 let mut invalid_utf8_chunk = BufReader::new(&b"\xff\n"[..]);
999 let error = read_chunked_body(&mut invalid_utf8_chunk, false).unwrap_err();
1000 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
1001 }
1002
1003 #[test]
1004 fn rejects_excessive_header_count() {
1005 let mut wire = String::from("HTTP/1.1 200 OK\r\n");
1006 for index in 0..(super::MAX_HEADER_COUNT + 1) {
1007 wire.push_str(&format!("X-{index}: v\r\n"));
1008 }
1009 wire.push_str("Content-Length: 0\r\n\r\n");
1010 let error = parse_response_bytes(wire.as_bytes(), Method::Get).unwrap_err();
1011 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
1012 }
1013
1014 #[test]
1015 fn rejects_overly_long_lines() {
1016 let long_value = "a".repeat(super::MAX_LINE_BYTES + 1);
1017 let wire = format!("HTTP/1.1 200 OK\r\nX-Test: {long_value}\r\n\r\n");
1018 let error = parse_response_bytes(wire.as_bytes(), Method::Get).unwrap_err();
1019 assert!(matches!(error, NanoGetError::MalformedHeader(_)));
1020 }
1021
1022 #[test]
1023 fn content_length_body_reader_handles_huge_lengths_without_preallocation() {
1024 let mut empty = io::empty();
1025 let error = read_content_length_body(&mut empty, usize::MAX).unwrap_err();
1026 assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
1027 }
1028
1029 #[test]
1030 fn deterministic_response_parser_fuzz_harness_is_panic_free() {
1031 let corpus: &[&[u8]] = &[
1032 b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
1033 b"HTTP/1.1 204 No Content\r\n\r\n",
1034 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
1035 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
1036 b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nok",
1037 b"HTP/1.1 200 OK\r\n\r\n",
1038 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n",
1039 b"",
1040 b"HTTP/1.1 200 OK\r\nContent-Length: 999999999999999999999\r\n\r\n",
1041 ];
1042
1043 for seed in corpus {
1044 let run = panic::catch_unwind(AssertUnwindSafe(|| {
1045 let _ = parse_response_bytes(seed, Method::Get);
1046 }));
1047 assert!(run.is_ok(), "parser panicked on corpus input");
1048 }
1049
1050 let mut rng = DeterministicRng::new(0xC0FFEE_F00DBAAD);
1051 for _ in 0..3_000 {
1052 let mut bytes = rng.next_bytes(512);
1053 if !bytes.is_empty() && (rng.next_u32() & 1) == 0 {
1054 let idx = (rng.next_u32() as usize) % bytes.len();
1055 bytes[idx] = bytes[idx].wrapping_add(1);
1056 }
1057 let method = if (rng.next_u32() & 1) == 0 {
1058 Method::Get
1059 } else {
1060 Method::Head
1061 };
1062 let run = panic::catch_unwind(AssertUnwindSafe(|| {
1063 let _ = parse_response_bytes(&bytes, method);
1064 }));
1065 assert!(run.is_ok(), "parser panicked for fuzz case");
1066 }
1067 }
1068}