Initial commit

This commit is contained in:
Tove 2025-09-07 16:39:28 +02:00
commit 0f284999ba
Signed by: TudbuT
GPG key ID: B3CF345217F202D3
6 changed files with 316 additions and 0 deletions

2
.envrc Normal file
View file

@ -0,0 +1,2 @@
use nix
export AUTH=testing

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.direnv/

16
Cargo.lock generated Normal file
View file

@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "horrorhttp"
version = "0.1.0"
dependencies = [
"readformat",
]
[[package]]
name = "readformat"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1fd097dab477324dfeb476d75627e38fd1a7437bc0e94751084be499d7e6b1"

10
Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "horrorhttp"
description = "A perhaps too flexible HTTP library based on a state machine."
license = "MIT"
repository = "https://git.tudbut.de/tudbut/horrorhttp"
version = "0.1.0"
edition = "2024"
[dependencies]
readformat = "1.0.1"

11
shell.nix Normal file
View file

@ -0,0 +1,11 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
rustc
cargo
helix
rust-analyzer
cargo-watch
sqlite-interactive
];
}

275
src/lib.rs Normal file
View file

@ -0,0 +1,275 @@
//! # HorrorHTTP
//!
//! **Please consider not using this library. It is not
//! meant for general use in e.g. a website, but it is very very flexible**
//!
//! HorrorHTTP is an extremely light HTTP library meant to allow for complete
//! flexibility in what you want to do with the connection.
//!
//! To start, feed a TcpStream and handler state into [`handle`]. This creates
//! a state machine which allows for each state to transition into any other state.
use std::{
collections::HashMap,
io::{Read, Write},
net::TcpStream,
thread::{self, JoinHandle},
};
use readformat::readf;
pub trait ConnectionState {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>>;
}
impl<T> ConnectionState for T
where
T: FnMut(&mut ClientHolder) -> Option<Box<dyn ConnectionState>>,
{
fn handle(mut self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
self(client)
}
}
/// Reads the head, and potentially part of the body. Hands control to [`ParseHeadState`].
pub struct BeginState;
impl ConnectionState for BeginState {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
let mut read_so_far = String::new();
let mut buf = [0u8; 1024];
while let Ok(n) = client.socket.read(&mut buf[..]) {
let index_of_current_buf = read_so_far.len();
let s = String::from_utf8_lossy(&buf[..n]);
read_so_far += &s;
if let Some(end) = read_so_far.find("\r\n\r\n") {
let index_in_buf = (end as isize - index_of_current_buf as isize + 4) as usize;
client.body.extend_from_slice(&buf[index_in_buf..n]);
let _ = read_so_far.split_off(end);
}
}
Some(Box::new(ParseHeadState(read_so_far)))
}
}
/// Parses the head and hands control to the body_reader.
pub struct ParseHeadState(String);
impl ConnectionState for ParseHeadState {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
let head = self.0.replace("\r\n", "\n");
let mut lines = head.lines();
let head_line = lines.next()?;
for (name, value) in lines
.map(|x| {
let mut x = x.splitn(2, ": ");
(x.next(), x.next())
})
.filter(|(a, b)| a.is_some() && b.is_some())
.map(|(a, b)| (a.unwrap(), b.unwrap()))
{
client.headers.insert(name.to_owned(), value.to_owned());
}
let head_line = readf("{} {} HTTP/{}", head_line)?;
let [m, p, h] = head_line.as_slice() else {
return None;
};
client.method = m.to_owned();
client.path = p.to_owned();
client.http_version = h.to_owned();
client
.body_reader
.take()
.or_else(|| client.custom_handler_state.take())
}
}
pub trait BodyReader: ConnectionState {}
/// Hands control further along.
pub struct BodyReaderDoNothing;
impl ConnectionState for BodyReaderDoNothing {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
client.custom_handler_state.take()
}
}
impl BodyReader for BodyReaderDoNothing {}
/// Reads the body based on Content-Length header.
pub struct BodyReaderHeader;
impl ConnectionState for BodyReaderHeader {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
let n = client
.headers
.get("Content-Length")
.map(|x| x.as_str())
.unwrap_or_else(|| "0")
.parse::<usize>()
.ok()?;
let n = n - client.body.len();
let mut buf = vec![0u8; n];
client.socket.read_exact(&mut buf).ok()?;
client.body.append(&mut buf);
client.custom_handler_state.take()
}
}
impl BodyReader for BodyReaderHeader {}
/// Reads the body until EOF (single-side shutdown).
pub struct BodyReaderEOF;
impl ConnectionState for BodyReaderEOF {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
client.socket.read_to_end(&mut client.body).ok()?;
client.custom_handler_state.take()
}
}
impl BodyReader for BodyReaderEOF {}
/// State that writes a response to the socket and then returns control to
/// the custom_handler_state.
pub struct ResponseWriter {
pub http_version: String,
pub status: u32,
pub status_text: String,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
impl ResponseWriter {
pub fn new() -> Self {
Self {
http_version: "1.0".to_owned(),
status: 200,
status_text: "OK".to_owned(),
headers: HashMap::new(),
body: None,
}
}
pub fn with_status(self, code: u32, text: impl Into<String>) -> Self {
Self {
status: code,
status_text: text.into(),
..self
}
}
pub fn with_version(self, ver: impl Into<String>) -> Self {
Self {
http_version: ver.into(),
..self
}
}
pub fn with_header(mut self, name: impl Into<String>, val: impl Into<String>) -> Self {
self.headers.insert(name.into(), val.into());
self
}
pub fn with_body(self, body: Vec<u8>) -> Self {
Self {
body: Some(body),
..self
}
}
pub fn without_body(self) -> Self {
Self { body: None, ..self }
}
}
impl ConnectionState for ResponseWriter {
fn handle(self: Box<Self>, client: &mut ClientHolder) -> Option<Box<dyn ConnectionState>> {
let mut buf = Vec::new();
buf.append(
&mut format!(
"HTTP/{} {} {}\r\n",
self.http_version, self.status, self.status_text
)
.into_bytes(),
);
let mut has_content_length = false;
for header in self.headers {
if header.0 == "Content-Length" {
has_content_length = true;
}
buf.append(&mut format!("{}: {}\r\n", header.0, header.1).into_bytes());
}
if let Some(mut body) = self.body {
if !has_content_length {
buf.append(&mut format!("Content-Length: {}\r\n", body.len()).into_bytes());
}
buf.extend_from_slice("\r\n".as_bytes());
buf.append(&mut body);
}
client.socket.write_all(&buf).ok()?;
client.custom_handler_state.take()
}
}
pub struct ClientHolder {
pub body_reader: Option<Box<dyn ConnectionState>>,
/// whenever giving control to a horrorhttp state, this will eventually be returned to.
pub custom_handler_state: Option<Box<dyn ConnectionState>>,
pub socket: TcpStream,
pub method: String,
pub path: String,
pub http_version: String,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
const EMPTY: String = String::new();
/// Begins handling a TCP connection. You must choose
/// a body reader and a handler. The body reader can
/// be any of [`BodyReaderDoNothing`], which does not
/// read the body any further than the head reader has
/// already read, [`BodyReaderHeader`], which reads as
/// many bytes as specified by Content-Length,
/// or [`BodyReaderEOF`], which reads until the input
/// closes. Custom BodyReaders may also be used.
///
/// The handler is written into the custom_handler_state
/// field of the ClientHolder, which will be called after
/// the body reader and then be reset to None. Each time
/// control is given to HorrorHTTP, it will at some point
/// return to custom_handler_state, if it is Some(...) at
/// that point - including after [`ResponseWriter`].
///
/// Control flow for a correct request and HorrorHTTP body
/// reader:
/// [`BeginState`] -> [`ParseHeadState`] -> dyn [`BodyReader`] ->
/// *handler* (-> [`ResponseWriter`] -> *custom_handler_state* ...)
pub fn handle(
stream: TcpStream,
body_reader: impl BodyReader + Send + 'static,
handler: impl ConnectionState + Send + 'static,
) -> JoinHandle<()> {
thread::spawn(move || {
let mut clienten = ClientHolder {
body_reader: Some(Box::new(body_reader)),
custom_handler_state: Some(Box::new(handler)),
socket: stream,
method: EMPTY,
path: EMPTY,
http_version: EMPTY,
headers: HashMap::new(),
body: Vec::new(),
};
let mut state: Box<dyn ConnectionState> = Box::new(BeginState);
loop {
if let Some(x) = state.handle(&mut clienten) {
state = x;
} else {
break;
}
}
})
}