use std::{
io::{Read, Write},
net::{TcpStream, ToSocketAddrs},
time::Duration,
};
use crate::errors::{
WardError, ERROR_HTTP_CODE_MISSING, ERROR_HTTP_VERSION_MISSING, ERROR_IP_LOOKUP,
ERROR_PARSING_HTTP_CODE, ERROR_SPLIT_HEADERS, ERROR_SPLIT_RAW,
};
const CONNECITON_TIMEOUT: u64 = 3000;
pub struct Response {
pub status_code: u16,
pub body: String,
}
impl<'a> Response {
pub fn from_str(data: &'a str) -> Result<Self, WardError> {
let (headers, body) = match data.split_once("\r\n\r\n") {
Some((h, b)) => (h, b),
None => return Err(WardError::HTTPError(ERROR_SPLIT_RAW.into())),
};
let (http_version_code_status, _content_type_line) = match headers.split_once("\r\n") {
Some((s, ct)) => (s, ct),
None => return Err(WardError::HTTPError(ERROR_SPLIT_HEADERS.to_string())),
};
let mut splitted_headers = http_version_code_status.splitn(3, " ");
if let None = splitted_headers.next() {
return Err(WardError::HTTPError(ERROR_HTTP_VERSION_MISSING.to_string()));
};
let status_code = match splitted_headers.next() {
Some(c) => match c.parse::<u16>() {
Ok(code) => Ok(code),
Err(err) => Err(WardError::HTTPError(format!(
"{ERROR_PARSING_HTTP_CODE}: {err}"
))),
}?,
None => return Err(WardError::HTTPError(ERROR_HTTP_CODE_MISSING.to_string())),
};
let response = Response {
status_code,
body: body.to_owned(),
};
Ok(response)
}
pub(crate) fn check_status_code(&self) -> Result<(), WardError> {
match self.status_code {
200 => Ok(()),
_ => return Err(WardError::HTTPError(self.body.trim().to_string())),
}
}
}
pub(crate) fn send_request(host: &str, headers: &[u8]) -> Result<String, WardError> {
let ip_lookup = host
.to_socket_addrs()
.map_err(|err| WardError::NetworkError(err.to_string()))?
.next();
if ip_lookup.is_none() {
return Err(WardError::NetworkError(ERROR_IP_LOOKUP.to_string()));
}
let mut socket = TcpStream::connect_timeout(
&ip_lookup.unwrap(),
Duration::from_millis(CONNECITON_TIMEOUT),
)
.map_err(|err| WardError::NetworkError(err.to_string()))?;
socket.write(headers).unwrap();
socket.flush().unwrap();
let mut buf = String::new();
socket
.read_to_string(&mut buf)
.map_err(|err| WardError::NetworkError(err.to_string()))?;
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn response_from_str() {
let data = "HTTP/1.0 200 OK\r\nContent-Type:text/plain\r\n\r\nResponse Body Here";
let response = Response::from_str(data).unwrap();
assert_eq!(response.status_code, 200);
assert_eq!(response.body, "Response Body Here");
}
}