#![allow(clippy::large_enum_variant)]
use super::{HttpHeaders, HttpRequest};
use crate::error::{Error, InvalidFirstLineError, InvalidResponseError};
use std::io::BufRead;
#[derive(Clone, Debug)]
pub struct HttpResponse {
version: String,
status_code: u16,
reason: String,
headers: HttpHeaders,
body: String,
}
impl HttpResponse {
pub fn new(status: &u16, headers: &Vec<String>, body: &String) -> Self {
Self::new_full(
status,
&HttpHeaders::from_vec(headers),
body,
&"1.1".to_string(),
&"".to_string(),
)
}
pub fn new_full(
status: &u16,
headers: &HttpHeaders,
body: &String,
version: &String,
reason: &String,
) -> Self {
Self {
version: version.clone(),
status_code: *status,
reason: reason.clone(),
headers: headers.clone(),
body: body.trim().trim_end_matches('0').to_string(),
}
}
pub fn version(&self) -> String {
self.version.clone()
}
pub fn status_code(&self) -> u16 {
self.status_code
}
pub fn reason(&self) -> String {
self.reason.clone()
}
pub fn headers(&self) -> HttpHeaders {
self.headers.clone()
}
pub fn body(&self) -> String {
self.body.clone()
}
pub fn raw(&self) -> String {
let headers_str = self
.headers
.all()
.iter()
.map(|(key, value)| format!("{}: {}", key, value.join("; ")))
.collect::<Vec<String>>()
.join("\r\n");
let res = format!(
"HTTP/{} {} {}\r\n{}\n\n{}\n\n",
self.version, self.status_code, self.reason, &headers_str, self.body
);
res.to_string()
}
pub fn read_header(
reader: &mut Box<dyn BufRead>,
req: &HttpRequest,
dest_file: &str,
) -> Result<Self, Error> {
let mut first_line = String::new();
match reader.read_line(&mut first_line) {
Ok(_) => {}
Err(e) => {
return Err(Error::NoRead(InvalidResponseError {
url: req.url.clone(),
response: e.to_string(),
}));
}
};
let (version, status, reason) = Self::parse_first_line(&first_line, req)?;
let mut header_lines = Vec::new();
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(_) => {}
Err(e) => {
return Err(Error::NoRead(InvalidResponseError {
url: req.url.clone(),
response: e.to_string(),
}));
}
};
if line.trim().is_empty() {
break;
}
header_lines.push(line.trim().to_string());
}
let headers = HttpHeaders::from_vec(&header_lines);
if headers.has_lower("transfer-encoding")
&& headers.get_lower("transfer-encoding").unwrap().as_str() == "chunked"
{
let mut _tmp = String::new();
reader.read_line(&mut _tmp).unwrap();
}
let mut body = String::new();
if dest_file.is_empty() {
reader.read_to_string(&mut body);
}
let res = Self::new_full(&status, &headers, &body, &version, &reason);
Ok(res)
}
pub fn parse_first_line(
first_line: &str,
req: &HttpRequest,
) -> Result<(String, u16, String), Error> {
let mut is_valid = true;
let parts = first_line
.trim_start_matches("HTTP/")
.split(' ')
.collect::<Vec<&str>>();
if !["1.0", "1.1", "2", "3"].contains(&parts[0]) {
is_valid = false;
} else if parts[1].len() != 3 || !parts[1].chars().all(|c| c.is_ascii_digit()) {
is_valid = false;
}
if !is_valid {
let error = InvalidFirstLineError {
request: req.clone(),
first_line: first_line.to_string(),
};
return Err(Error::InvalidFirstLine(error));
}
Ok((
parts[0].to_string(),
parts[1].parse::<u16>().unwrap(),
parts[2].to_string(),
))
}
}