use std::{
fmt::Display,
fs,
io::{BufRead, BufReader, Read, Write},
net::TcpStream,
};
use yan_json::prelude::*;
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub enum HttpMethod {
Get,
Post,
}
impl TryFrom<&str> for HttpMethod {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"GET" => Ok(Self::Get),
"POST" => Ok(Self::Post),
_ => Err(()),
}
}
}
impl Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HttpBody {
Json(JsonNode),
Html(String),
None,
}
impl HttpBody {
pub fn parse(body_type: &str, body_raw: Vec<u8>) -> Option<Self> {
match body_type {
"application/json" => {
let json_str = String::from_utf8(body_raw).ok()?;
Some(HttpBody::Json(JsonNode::parse(&json_str)?))
}
"text/html" => Some(Self::Html(String::from_utf8(body_raw).ok()?)),
"None" => Some(HttpBody::None),
_ => None,
}
}
pub fn create_html(source: &str) -> Self {
Self::Html(String::from_utf8(fs::read(source).unwrap()).unwrap())
}
pub fn get_type(&self) -> String {
match self {
Self::None => "no-body-type",
Self::Json(_) => "application/json",
Self::Html(_) => "text/html",
}
.into()
}
pub fn as_json(&self) -> Option<&JsonNode> {
match self {
Self::Json(node) => Some(node),
_ => None,
}
}
pub fn get_byte_count(&self) -> usize {
match self {
Self::None => 0,
Self::Json(node) => node.to_string().len(),
Self::Html(data) => data.len(),
}
}
}
impl TryFrom<HttpBody> for JsonNode {
type Error = ();
fn try_from(body: HttpBody) -> Result<Self, Self::Error> {
match body {
HttpBody::Json(root) => Ok(root),
_ => Err(()),
}
}
}
#[derive(Debug)]
pub struct Request {
pub method: HttpMethod,
pub path: String,
pub headers: HashMap<String, String>,
pub body: HttpBody,
}
impl Request {
pub fn send(&self) -> Option<Response> {
let Some(destination) = self.headers.get("Host") else {
println!("Host header not found");
return None;
};
let mut stream = match TcpStream::connect(destination) {
Ok(stream) => stream,
Err(e) => panic!("{e}"),
};
let first_line = format!("{} {} HTTP/1.1", self.method, self.path);
stream.write_all(first_line.as_bytes()).ok()?;
stream.write_all(b"\r\n").ok()?;
for (key, val) in &self.headers {
let header_line = format!("{key}: {val}\r\n");
stream.write_all(header_line.as_bytes()).ok()?;
}
stream.write_all(b"\r\n").ok()?;
match &self.body {
HttpBody::Json(node) => {
stream.write_all(node.to_string().as_bytes()).ok()?;
}
HttpBody::Html(data) => {
stream.write_all(data.as_bytes()).ok()?;
}
HttpBody::None => {}
}
Response::parse(&mut stream)
}
pub fn parse(stream: &mut TcpStream) -> Option<Self> {
let mut br = BufReader::new(stream);
let mut lines = br.by_ref().lines();
let req_status_line: String = lines.next()?.ok()?;
let mut it = req_status_line.split(' ');
let method: HttpMethod = it.next()?.try_into().ok()?;
let path: String = it.next().unwrap().into();
let headers = lines
.map(|l| l.ok())
.take_while(|l| matches!(l, Some(l) if !l.trim().is_empty()))
.map(|l| {
let l = l?;
let mut it = l.split(": ");
Some((it.next()?.to_lowercase(), it.next()?.to_lowercase()))
})
.collect::<Option<HashMap<String, String>>>()?;
let body_type: &str = headers
.get("content-type")
.map(|s| s.as_str())
.unwrap_or("None");
let body_byte_count: usize = headers
.get("content-length")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let mut body_raw = vec![0_u8; body_byte_count];
br.read_exact(&mut body_raw).unwrap();
let body = HttpBody::parse(body_type, body_raw)?;
Some(Self {
method,
headers,
path,
body,
})
}
}
#[derive(Default)]
pub struct RequestBuilder {
path: Option<String>,
method: Option<HttpMethod>,
headers: HashMap<String, String>,
body: Option<HttpBody>,
}
impl RequestBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn build(self) -> Request {
Request {
method: self.method.unwrap(),
path: self.path.unwrap(),
headers: self.headers,
body: self.body.unwrap(),
}
}
pub fn method(mut self, method: HttpMethod) -> Self {
self.method = Some(method);
self
}
pub fn path(mut self, path: &str) -> Self {
self.path = Some(path.to_string());
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
*self.headers.entry(key.to_string()).or_default() = value.to_string();
self
}
pub fn body_and_auto_content_headers(self, body: HttpBody) -> Self {
if body == HttpBody::None {
return self.body(HttpBody::None);
}
self.header("Content-Type", &body.get_type())
.header("Content-Length", &body.get_byte_count().to_string())
.body(body)
}
fn body(mut self, body: HttpBody) -> Self {
self.body = Some(body);
self
}
}
#[derive(Debug)]
pub struct Response {
pub status: (usize, String),
pub headers: HashMap<String, String>,
pub body: HttpBody,
}
impl Response {
pub fn write_to_stream(&self, stream: &mut TcpStream) {
let status_line: String = format!("HTTP/1.1 {} {}", self.status.0, self.status.1);
stream.write_all(status_line.as_bytes()).unwrap();
stream.write_all(b"\r\n").unwrap();
for (key, val) in &self.headers {
stream.write_all(key.as_bytes()).unwrap();
stream.write_all(b": ").unwrap();
stream.write_all(val.as_bytes()).unwrap();
stream.write_all(b"\r\n").unwrap();
}
stream.write_all(b"\r\n").unwrap();
match &self.body {
HttpBody::Json(node) => {
stream.write_all(node.to_string().as_bytes()).unwrap();
}
HttpBody::Html(data) => {
stream.write_all(data.as_bytes()).unwrap();
}
HttpBody::None => {}
}
}
pub fn parse(stream: &mut TcpStream) -> Option<Self> {
let mut br = BufReader::new(stream);
let mut lines = br.by_ref().lines();
let req_status_line = lines.next()?;
let req_status_line = req_status_line.ok()?;
let mut it = req_status_line.split(' ');
let _http_version = it.next()?;
let status_code = it.next()?.parse::<usize>().ok()?;
let status_text = it.next()?.to_string();
let headers = lines
.map(|l| l.ok())
.take_while(|l| matches!(l, Some(l) if !l.trim().is_empty()))
.map(|l| {
let l = l?;
let mut it = l.split(": ");
Some((it.next()?.to_lowercase(), it.next()?.to_lowercase()))
})
.collect::<Option<HashMap<String, String>>>()?;
let body_type: &str = headers
.get("content-type")
.map(|s| s.as_str())
.unwrap_or("None");
let body_byte_count: usize = headers
.get("content-length")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let mut body_raw = vec![0_u8; body_byte_count];
br.read_exact(&mut body_raw).unwrap();
let body = HttpBody::parse(body_type, body_raw)?;
Some(Self {
status: (status_code, status_text),
headers,
body,
})
}
}
#[derive(Default)]
pub struct ResponseBuilder {
status: Option<(usize, String)>,
headers: HashMap<String, String>,
body: Option<HttpBody>,
}
impl ResponseBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn build(self) -> Response {
Response {
status: self.status.unwrap(),
headers: self.headers,
body: self.body.unwrap(),
}
}
pub fn status_ok(self) -> Self {
self.status(200, "OK")
}
pub fn status(mut self, code: usize, text: &str) -> Self {
self.status = Some((code, text.to_string()));
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
*self.headers.entry(key.to_string()).or_default() = value.to_string();
self
}
pub fn body_and_auto_content_headers(self, body: HttpBody) -> Self {
if body == HttpBody::None {
return self.body(HttpBody::None);
}
self.header("Content-Type", &body.get_type())
.header("Content-Length", &body.get_byte_count().to_string())
.body(body)
}
fn body(mut self, body: HttpBody) -> Self {
self.body = Some(body);
self
}
}