first commit

This commit is contained in:
2025-11-09 01:06:25 +02:00
commit dbaa2feb48
28 changed files with 1551 additions and 0 deletions

243
stdsrv/src/request.rs Normal file
View File

@@ -0,0 +1,243 @@
//! An abstraction layer for parsing HTTP requests. As simple and high level as I managed.
use crate::error::{Error, ErrorKind, Result};
use crate::http_header::HttpHeaders;
use crate::log;
use crate::logger::Level;
use std::fmt::Display;
use std::io::{BufRead, BufReader};
use std::net::TcpStream;
use std::path::PathBuf;
/// Supports just GET methods for now.
#[derive(Debug, PartialEq)]
#[allow(dead_code, clippy::upper_case_acronyms)]
pub enum HttpMethod {
GET,
//PUT,
//POST,
}
impl TryFrom<&str> for HttpMethod {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
match value {
"GET" => Ok(Self::GET),
//"PUT" => Ok(Self::PUT),
//"POST" => Ok(Self::POST),
_ => Err(Error::new(ErrorKind::UnsupportedHttpMethod, value)),
}
}
}
impl Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/// Type for HTTP requests.
#[derive(Debug)]
#[allow(dead_code)]
pub struct HttpRequest {
pub method: HttpMethod,
pub path: PathBuf,
pub version: String,
pub headers: HttpHeaders,
}
impl Display for HttpRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {}\n{}",
self.method,
self.path.display(),
self.version,
self.headers
)
}
}
impl TryFrom<&mut BufReader<&TcpStream>> for HttpRequest {
type Error = Error;
fn try_from(value: &mut BufReader<&TcpStream>) -> std::result::Result<Self, Self::Error> {
let mut request_line = String::new();
value
.read_line(&mut request_line)
.map_err(|_e| Error::new(ErrorKind::RequestParse, "expected request line"))?;
let mut parts = request_line.split_whitespace();
let method: HttpMethod = parts
.next()
.ok_or(Error::new(
ErrorKind::RequestParse,
"expected request method",
))?
.try_into()?;
let path_with_prefix: PathBuf = parts
.next()
.ok_or(Error::new(ErrorKind::RequestParse, "expected request path"))?
.into();
let path = if path_with_prefix.starts_with("/") {
path_with_prefix.strip_prefix("/").unwrap().into()
} else {
path_with_prefix
};
let version = parts
.next()
.ok_or(Error::new(
ErrorKind::RequestParse,
"expected request version",
))?
.into();
let mut req = Self {
method,
path,
version,
headers: HttpHeaders::new(),
};
loop {
let mut line = String::new();
value.read_line(&mut line)?;
if line == "\r\n" || line.is_empty() {
break;
}
if let Some((header, val)) = line.split_once(": ") {
req.headers.add(header, val);
}
}
Ok(req)
}
}
impl TryFrom<&str> for HttpRequest {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
let mut lines = s.split("\r\n");
let request_line = lines
.next()
.ok_or(Error::new(ErrorKind::RequestParse, "expected request line"))?;
let mut parts = request_line.split_whitespace();
let method = parts
.next()
.ok_or(Error::new(
ErrorKind::RequestParse,
"expected request method",
))?
.try_into()?;
let path_with_prefix: PathBuf = parts
.next()
.ok_or(Error::new(ErrorKind::RequestParse, "expected request path"))?
.into();
let path = if path_with_prefix.starts_with("/") {
path_with_prefix.strip_prefix("/").unwrap().into()
} else {
path_with_prefix
};
let version = parts
.next()
.ok_or(Error::new(
ErrorKind::RequestParse,
"expected request version",
))?
.into();
let mut headers = HttpHeaders::new();
for line in lines {
if let Some(v) = line.split_once(": ") {
headers.add(v.0, v.1)
}
}
let req = Self {
method,
path,
version,
headers,
};
log!(Level::Info, "\n{}", req);
Ok(req)
}
}
// ------------------------------
// TESTS
// ------------------------------
#[cfg(test)]
mod request_test {
use super::*;
#[test]
fn http_parse_method_get() {
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
let req: HttpRequest = s.try_into().unwrap();
assert_eq!(req.method, HttpMethod::GET);
}
#[test]
fn http_parse_path_indexhtml() {
let s = "GET /index.html HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
let req: HttpRequest = s.try_into().unwrap();
assert_eq!(req.path, PathBuf::from("index.html"));
}
#[test]
fn http_parse_version() {
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
let req: HttpRequest = s.try_into().unwrap();
assert_eq!(req.version, "HTTP/1.1");
}
#[test]
fn http_parse_headers_len() {
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
let req: HttpRequest = s.try_into().unwrap();
assert_eq!(req.headers.len(), 3);
}
#[test]
fn http_parse_useragent_header_curl() {
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
let req: HttpRequest = s.try_into().unwrap();
assert_eq!(req.headers.get("User-Agent").unwrap(), "curl/8.14.1");
}
#[test]
fn http_parse_empty_should_fail() {
let s = "";
let req: Result<HttpRequest> = s.try_into();
assert!(req.is_err());
}
#[test]
fn http_parse_unsupported_method_delete() {
let s = "DELETE / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
let req: Result<HttpRequest> = s.try_into();
assert!(req.is_err());
}
#[test]
fn http_method_display() {
let method = HttpMethod::GET;
assert_eq!(method.to_string(), "GET")
}
}