use crate::{connection::HttpStream, Error};
use std::collections::HashMap;
use std::io::{Bytes, Read};
use std::str;
#[derive(Clone, PartialEq, Debug)]
pub struct Response {
pub status_code: i32,
pub reason_phrase: String,
pub headers: HashMap<String, String>,
body: Vec<u8>,
}
impl Response {
pub(crate) fn create(mut parent: ResponseLazy) -> Result<Response, Error> {
let mut body = Vec::new();
if parent.status_code != 204 && parent.status_code != 304 {
for byte in &mut parent {
let (byte, length) = byte?;
body.reserve(length);
body.push(byte);
}
}
let ResponseLazy {
status_code,
reason_phrase,
headers,
..
} = parent;
Ok(Response {
status_code,
reason_phrase,
headers,
body,
})
}
pub fn as_str(&self) -> Result<&str, Error> {
match str::from_utf8(&self.body) {
Ok(s) => Ok(s),
Err(err) => Err(Error::InvalidUtf8InBody(err)),
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.body
}
pub fn into_bytes(self) -> Vec<u8> {
self.body
}
}
pub struct ResponseLazy {
pub status_code: i32,
pub reason_phrase: String,
pub headers: HashMap<String, String>,
stream: Bytes<HttpStream>,
state: HttpStreamState,
}
impl ResponseLazy {
pub(crate) fn from_stream(stream: HttpStream) -> Result<ResponseLazy, Error> {
let mut stream = stream.bytes();
let ResponseMetadata {
status_code,
reason_phrase,
headers,
state,
} = read_metadata(&mut stream)?;
Ok(ResponseLazy {
status_code,
reason_phrase,
headers,
stream,
state,
})
}
}
impl Iterator for ResponseLazy {
type Item = Result<(u8, usize), Error>;
fn next(&mut self) -> Option<Self::Item> {
use HttpStreamState::*;
match self.state {
EndOnClose => read_until_closed(&mut self.stream),
ContentLength(ref mut length) => read_with_content_length(&mut self.stream, length),
Chunked(ref mut expecting_chunks, ref mut length, ref mut content_length) => {
read_chunked(
&mut self.stream,
&mut self.headers,
expecting_chunks,
length,
content_length,
)
}
}
}
}
fn read_until_closed(bytes: &mut Bytes<HttpStream>) -> Option<<ResponseLazy as Iterator>::Item> {
if let Some(byte) = bytes.next() {
match byte {
Ok(byte) => Some(Ok((byte, 1))),
Err(err) => Some(Err(Error::IoError(err))),
}
} else {
None
}
}
fn read_with_content_length(
bytes: &mut Bytes<HttpStream>,
content_length: &mut usize,
) -> Option<<ResponseLazy as Iterator>::Item> {
if *content_length > 0 {
*content_length -= 1;
if let Some(byte) = bytes.next() {
match byte {
Ok(byte) => return Some(Ok((byte, *content_length + 1))),
Err(err) => return Some(Err(Error::IoError(err))),
}
}
}
None
}
fn read_trailers(
bytes: &mut Bytes<HttpStream>,
headers: &mut HashMap<String, String>,
) -> Result<(), Error> {
loop {
let trailer_line = read_line(bytes)?;
if let Some((header, value)) = parse_header(trailer_line) {
headers.insert(header, value);
} else {
break;
}
}
Ok(())
}
fn read_chunked(
bytes: &mut Bytes<HttpStream>,
headers: &mut HashMap<String, String>,
expecting_more_chunks: &mut bool,
chunk_length: &mut usize,
content_length: &mut usize,
) -> Option<<ResponseLazy as Iterator>::Item> {
if !*expecting_more_chunks && *chunk_length == 0 {
return None;
}
if *chunk_length == 0 {
let length_line = match read_line(bytes) {
Ok(line) => line,
Err(err) => return Some(Err(err)),
};
match usize::from_str_radix(&length_line, 16) {
Ok(incoming_length) => {
if incoming_length == 0 {
if let Err(err) = read_trailers(bytes, headers) {
return Some(Err(err));
}
*expecting_more_chunks = false;
headers.insert("content-length".to_string(), (*content_length).to_string());
headers.remove("transfer-encoding");
return None;
}
*chunk_length = incoming_length;
*content_length += incoming_length;
}
Err(_) => return Some(Err(Error::MalformedChunkLength)),
}
}
if *chunk_length > 0 {
*chunk_length -= 1;
if let Some(byte) = bytes.next() {
match byte {
Ok(byte) => {
if *chunk_length == 0 {
if let Err(err) = read_line(bytes) {
return Some(Err(err));
}
}
return Some(Ok((byte, *chunk_length + 1)));
}
Err(err) => return Some(Err(Error::IoError(err))),
}
}
}
None
}
enum HttpStreamState {
EndOnClose,
ContentLength(usize),
Chunked(bool, usize, usize),
}
struct ResponseMetadata {
status_code: i32,
reason_phrase: String,
headers: HashMap<String, String>,
state: HttpStreamState,
}
fn read_metadata(stream: &mut Bytes<HttpStream>) -> Result<ResponseMetadata, Error> {
let (status_code, reason_phrase) = parse_status_line(&read_line(stream)?);
let mut headers = HashMap::new();
loop {
let line = read_line(stream)?;
if line.is_empty() {
break;
}
if let Some(header) = parse_header(line) {
headers.insert(header.0, header.1);
}
}
let mut chunked = false;
let mut content_length = None;
for (header, value) in &headers {
if header.to_lowercase().trim() == "transfer-encoding"
&& value.to_lowercase().trim() == "chunked"
{
chunked = true;
}
if header.to_lowercase().trim() == "content-length" {
match str::parse::<usize>(value.trim()) {
Ok(length) => content_length = Some(length),
Err(_) => return Err(Error::MalformedContentLength),
}
}
}
let state = if chunked {
HttpStreamState::Chunked(true, 0, 0)
} else if let Some(length) = content_length {
HttpStreamState::ContentLength(length)
} else {
HttpStreamState::EndOnClose
};
Ok(ResponseMetadata {
status_code,
reason_phrase,
headers,
state,
})
}
fn read_line(stream: &mut Bytes<HttpStream>) -> Result<String, Error> {
let mut bytes = Vec::with_capacity(32);
for byte in stream {
let byte = byte?;
if byte == b'\n' {
bytes.pop();
break;
} else {
bytes.push(byte);
}
}
String::from_utf8(bytes).map_err(|_error| Error::InvalidUtf8InResponse)
}
fn parse_status_line(line: &str) -> (i32, String) {
let mut status_code = String::with_capacity(3);
let mut reason_phrase = String::with_capacity(2);
let mut spaces = 0;
for c in line.chars() {
if spaces >= 2 {
reason_phrase.push(c);
}
if c == ' ' {
spaces += 1;
} else if spaces == 1 {
status_code.push(c);
}
}
if let Ok(status_code) = status_code.parse::<i32>() {
if !reason_phrase.is_empty() {
return (status_code, reason_phrase);
}
}
(503, "Server did not provide a status line".to_string())
}
fn parse_header(mut line: String) -> Option<(String, String)> {
if let Some(location) = line.find(':') {
let value = if let Some(sp) = line.get(location + 1..location + 2) {
if sp == " " {
line[location + 2..].to_string()
} else {
line[location + 1..].to_string()
}
} else {
line[location + 1..].to_string()
};
line.truncate(location);
line.make_ascii_lowercase();
return Some((line, value));
}
None
}