use std::{collections::HashMap, net::IpAddr, str::FromStr};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Method {
GET,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE,
PATCH,
}
impl FromStr for Method {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"GET" => Ok(Method::GET),
"HEAD" => Ok(Method::HEAD),
"POST" => Ok(Method::POST),
"PUT" => Ok(Method::PUT),
"DELETE" => Ok(Method::DELETE),
"CONNECT" => Ok(Method::CONNECT),
"OPTIONS" => Ok(Method::OPTIONS),
"TRACE" => Ok(Method::TRACE),
"PATCH" => Ok(Method::PATCH),
_ => Err(anyhow::anyhow!("No matching HTTP method")),
}
}
}
impl ToString for Method {
fn to_string(&self) -> String {
match &self {
Method::GET => "GET".to_string(),
Method::HEAD => "HEAD".to_string(),
Method::POST => "POST".to_string(),
Method::PUT => "PUT".to_string(),
Method::DELETE => "DELETE".to_string(),
Method::CONNECT => "CONNECT".to_string(),
Method::OPTIONS => "OPTIONS".to_string(),
Method::TRACE => "TRACE".to_string(),
Method::PATCH => "PATCH".to_string(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Version {
V10,
V11,
V20,
V30,
}
impl FromStr for Version {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"HTTP/1.0" => Ok(Version::V10),
"HTTP/1.1" => Ok(Version::V11),
"HTTP/2.0" => Ok(Version::V20),
"HTTP/3.0" => Ok(Version::V30),
_ => Err(anyhow::anyhow!("No matching HTTP version")),
}
}
}
impl ToString for Version {
fn to_string(&self) -> String {
match &self {
Version::V10 => "HTTP/1.0".to_string(),
Version::V11 => "HTTP/1.1".to_string(),
Version::V20 => "HTTP/2.0".to_string(),
Version::V30 => "HTTP/3.0".to_string(),
}
}
}
#[derive(Debug)]
pub enum StatusCode {
CODE100, CODE102, CODE103, CODE200, CODE202, CODE204, CODE205, CODE206, CODE300, CODE301, CODE302, CODE303, CODE304, CODE307, CODE308, CODE400, CODE401, CODE403, CODE404, CODE405, CODE406, CODE408, CODE409, CODE500, CODE501, CODE502, CODE503, CODE504, CODE505, CODE511, }
impl ToString for StatusCode {
fn to_string(&self) -> String {
match self {
StatusCode::CODE100 => "100 Continue".to_string(),
StatusCode::CODE102 => "102 Processing".to_string(),
StatusCode::CODE103 => "103 Early Hints".to_string(),
StatusCode::CODE200 => "200 OK".to_string(),
StatusCode::CODE202 => "202 Accepted".to_string(),
StatusCode::CODE204 => "204 No Content".to_string(),
StatusCode::CODE205 => "205 Reset Content".to_string(),
StatusCode::CODE206 => "206 Partial Content".to_string(),
StatusCode::CODE300 => "300 Multiple Choices".to_string(),
StatusCode::CODE301 => "301 Moved Permanently".to_string(),
StatusCode::CODE302 => "302 Found".to_string(),
StatusCode::CODE303 => "303 See Other".to_string(),
StatusCode::CODE304 => "304 Not Modified".to_string(),
StatusCode::CODE307 => "307 Temporary Redirect".to_string(),
StatusCode::CODE308 => "308 Permanent Redirect".to_string(),
StatusCode::CODE400 => "400 Bad Request".to_string(),
StatusCode::CODE401 => "401 Unauthorized".to_string(),
StatusCode::CODE403 => "403 Forbidden".to_string(),
StatusCode::CODE404 => "404 Not Found".to_string(),
StatusCode::CODE405 => "405 Method Not Allowed".to_string(),
StatusCode::CODE406 => "406 Not Acceptable".to_string(),
StatusCode::CODE408 => "408 Request Timeout".to_string(),
StatusCode::CODE409 => "409 Conflict".to_string(),
StatusCode::CODE500 => "500 Internal Server Error".to_string(),
StatusCode::CODE501 => "501 Not Implemented".to_string(),
StatusCode::CODE502 => "502 Bad Gateway".to_string(),
StatusCode::CODE503 => "503 Service Unabailable".to_string(),
StatusCode::CODE504 => "504 Gateway Timeout".to_string(),
StatusCode::CODE505 => "505 HTTP Version Not Supported".to_string(),
StatusCode::CODE511 => "511 Network Authentication Required".to_string(),
}
}
}
#[derive(Debug)]
pub struct HTTPRequest {
pub method: Method,
pub path: String,
pub version: Version,
pub headers: HashMap<String, String>,
pub addr: IpAddr,
pub body: Option<String>,
}
impl FromStr for HTTPRequest {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut lines = s.lines();
let request_line = lines
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid request"))?;
let mut parts = request_line.split_whitespace();
let method: Method = parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing method"))?
.parse()?;
let path: String = parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing path"))?
.to_string();
let version: Version = parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing HTTP version"))?
.parse()?;
let mut headers = HashMap::new();
for line in lines.by_ref() {
if line.is_empty() {
break; }
let mut header_parts = line.splitn(2, ':');
let header_name = header_parts.next().unwrap().trim().to_string();
let header_value = header_parts
.next()
.ok_or_else(|| anyhow::anyhow!("Malformed header"))?
.trim()
.to_string();
headers.insert(header_name, header_value);
}
let body = if lines.clone().count() > 0 {
Some(lines.collect::<Vec<&str>>().join("\n"))
} else {
None
};
Ok(HTTPRequest {
method,
path,
version,
headers,
addr: IpAddr::from_str("0.0.0.0")?,
body,
})
}
}
impl HTTPRequest {
pub fn get_header(request: &HTTPRequest, header: &str) -> Option<String> {
match request.headers.get(header) {
Some(value) => Some(value.to_string()),
None => None,
}
}
pub fn get_cookies(request: &HTTPRequest) -> anyhow::Result<HashMap<String, String>> {
let mut cookies = HashMap::new();
let data = match HTTPRequest::get_header(request, "Cookie") {
Some(data) => data,
None => return Err(anyhow::anyhow!("No Cookie header found")),
};
for cookie in data.split(";") {
let mut split = cookie.split("=");
let name = split.next().unwrap();
let value = split.next().unwrap();
cookies.insert(name.to_string(), value.to_string());
}
Ok(cookies)
}
pub fn get_cookie(request: &HTTPRequest, cookie: &str) -> anyhow::Result<String> {
let cookies = HTTPRequest::get_cookies(request)?;
match cookies.get(cookie) {
Some(cookie) => Ok(cookie.to_string()),
None => {
return Err(anyhow::anyhow!(
"No cookie with this name ({}) exists",
cookie
));
}
}
}
}
#[derive(Debug)]
pub struct HTTPResponse {
pub version: Version,
pub status_code: StatusCode,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
impl ToString for HTTPResponse {
fn to_string(&self) -> String {
format!(
"{} {}\n{}",
self.version.to_string(),
self.status_code.to_string(),
self.headers
.iter()
.map(|(k, v)| { format!("{}: {}", k, v) })
.collect::<Vec<String>>()
.join("\n"),
)
}
}