use std::io::Read;
use super::{read_headers, read_line, ParseError};
use crate::header::Header;
use crate::parse::transfer::{resolve_body, Body, MessageKind, RequestMethod};
pub struct ParsedResponse {
pub proto: String,
pub proto_major: u8,
pub proto_minor: u8,
pub status: u16,
pub status_text: String,
pub header: Header,
pub body: Body,
pub content_length: i64,
pub transfer_encoding: Vec<String>,
}
impl std::fmt::Debug for ParsedResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParsedResponse")
.field("status", &self.status)
.field("status_text", &self.status_text)
.finish_non_exhaustive()
}
}
pub fn read_response(
r: impl Read + Send + 'static,
req_method: Option<&str>,
max_header_bytes: usize,
) -> Result<ParsedResponse, ParseError> {
let mut r: Box<dyn Read + Send> = Box::new(r);
let line = read_line(&mut r)?;
let sp1 = line.find(' ').ok_or(ParseError::BadStatusLine)?;
let proto = &line[..sp1];
let rest = line[sp1 + 1..].trim_start();
let (proto_major, proto_minor) = parse_proto(proto)?;
let (status_str, status_text) = match rest.find(' ') {
Some(i) => (&rest[..i], rest[i + 1..].to_owned()),
None => (rest, String::new()),
};
let status: u16 = status_str
.parse()
.map_err(|_| ParseError::BadStatusLine)?;
if !(100..=999).contains(&status) {
return Err(ParseError::BadStatusLine);
}
let header = read_headers(&mut r, max_header_bytes)?;
let transfer_encoding: Vec<String> = header
.values("Transfer-Encoding")
.iter()
.flat_map(|v| v.split(',').map(|s| s.trim().to_ascii_lowercase()))
.collect();
let content_length = match header.get("Content-Length") {
None => -1,
Some(s) => s.trim().parse::<i64>().map_err(|_| ParseError::InvalidContentLength)?,
};
let method = match req_method.map(|m| m.to_ascii_uppercase()).as_deref() {
Some("HEAD") => Some(RequestMethod::Head),
Some("CONNECT") => Some(RequestMethod::Connect),
_ => Some(RequestMethod::Other),
};
let body = resolve_body(
r,
&header,
MessageKind::Response { status, method },
)?;
Ok(ParsedResponse {
proto: proto.to_owned(),
proto_major,
proto_minor,
status,
status_text,
header,
body,
content_length,
transfer_encoding,
})
}
fn parse_proto(proto: &str) -> Result<(u8, u8), ParseError> {
if !proto.starts_with("HTTP/") {
return Err(ParseError::BadStatusLine);
}
let ver = &proto[5..];
let dot = ver.find('.').ok_or(ParseError::BadStatusLine)?;
let major: u8 = ver[..dot].parse().map_err(|_| ParseError::BadStatusLine)?;
let minor: u8 = ver[dot + 1..].parse().map_err(|_| ParseError::BadStatusLine)?;
Ok((major, minor))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Cursor, Read};
fn parse(raw: &'static [u8], method: Option<&str>) -> ParsedResponse {
read_response(Cursor::new(raw), method, 65536).unwrap()
}
#[test]
fn simple_200() {
let resp = parse(
b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello",
Some("GET"),
);
assert_eq!(resp.status, 200);
assert_eq!(resp.status_text, "OK");
assert_eq!(resp.proto_major, 1);
assert_eq!(resp.proto_minor, 1);
assert_eq!(resp.content_length, 5);
}
#[test]
fn head_response_no_body() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
let mut resp = read_response(Cursor::new(raw.as_ref()), Some("HEAD"), 65536).unwrap();
let mut out = Vec::new();
resp.body.read_to_end(&mut out).unwrap();
assert!(out.is_empty());
}
#[test]
fn no_content_204() {
let raw = b"HTTP/1.1 204 No Content\r\n\r\n";
let mut resp = parse(raw, Some("DELETE"));
let mut out = Vec::new();
resp.body.read_to_end(&mut out).unwrap();
assert!(out.is_empty());
}
#[test]
fn chunked_response() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n";
let mut resp = parse(raw, Some("GET"));
let mut out = Vec::new();
resp.body.read_to_end(&mut out).unwrap();
assert_eq!(out, b"Hello");
}
#[test]
fn bad_status_code() {
let result = read_response(
Cursor::new(b"HTTP/1.1 999 Bad\r\n\r\n"),
None,
65536,
);
assert!(result.is_ok(), "status 999 should be valid");
let result2 = read_response(
Cursor::new(b"HTTP/1.1 abc Bad\r\n\r\n"),
None,
65536,
);
assert_eq!(result2.unwrap_err(), ParseError::BadStatusLine);
}
}