Initial commit
This commit is contained in:
commit
0f284999ba
6 changed files with 316 additions and 0 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
use nix
|
||||
export AUTH=testing
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.direnv/
|
||||
16
Cargo.lock
generated
Normal file
16
Cargo.lock
generated
Normal 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
10
Cargo.toml
Normal 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
11
shell.nix
Normal 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
275
src/lib.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue