[uhttp] recv working

This commit is contained in:
soup 2024-10-10 00:34:40 -04:00
commit fd85c12da3
No known key found for this signature in database
13 changed files with 697 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

14
Cargo.lock generated Normal file
View file

@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "shelves"
version = "0.1.0"
dependencies = [
"uhttp",
]
[[package]]
name = "uhttp"
version = "0.1.0"

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[workspace]
members = [
"crates/uhttp"
]
resolver = "2"
[package]
name = "shelves"
version = "0.1.0"
edition = "2021"
[dependencies]
uhttp = { path = "crates/uhttp" }

6
crates/uhttp/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "uhttp"
version = "0.1.0"
edition = "2021"
[dependencies]

27
crates/uhttp/README.md Normal file
View file

@ -0,0 +1,27 @@
# uhttp
A small allocation-free, sans-IO HTTP implementation
## Goals
1. Enable development of HTTP-speaking applications
2. Simple to understand codebase
3. Fast to compile
## Non-goals
1. Ultimate speed
## Scope
### HTTP/1.1
???
### HTTP/2
???
## Non-scope
### HTTP/1.1
???
### HTTP/2
???

View file

@ -0,0 +1,41 @@
use crate::Error;
pub enum Eat<T> {
NeedMoreData,
Consumed {
amt: usize,
result: Result<T, Error>,
},
}
impl<T> Eat<T> {
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Eat<U> {
match self {
Eat::NeedMoreData => Eat::NeedMoreData,
Eat::Consumed {
amt,
result: Err(e),
} => Eat::Consumed {
amt,
result: Err(e),
},
Eat::Consumed { amt, result: Ok(v) } => Eat::Consumed {
amt,
result: Ok(f(v)),
},
}
}
pub fn and_then<U>(self, f: impl FnOnce(usize, T) -> Eat<U>) -> Eat<U> {
match self {
Eat::NeedMoreData => Eat::NeedMoreData,
Eat::Consumed {
amt,
result: Err(e),
} => Eat::Consumed {
amt,
result: Err(e),
},
Eat::Consumed { amt, result: Ok(v) } => f(amt, v),
}
}
}

182
crates/uhttp/src/lib.rs Normal file
View file

@ -0,0 +1,182 @@
#![no_std]
pub mod common;
pub mod parse;
pub mod parts;
pub use common::*;
pub use parts::*;
#[derive(Debug)]
pub enum Error {
InvalidConnectionState,
Parse(&'static str),
TrailingBytes,
}
#[derive(Default, Debug)]
pub enum Event<'a> {
#[default]
Empty,
RequestLine(RequestLine<'a>),
Header(Header<'a>),
HeadersEnd,
BodyChunk(&'a [u8]),
Done,
}
#[derive(Debug)]
pub enum Role {
Client,
Server,
}
#[derive(Debug, Copy, Clone)]
enum BodyState {
ContentLength(usize),
Chunked,
}
#[derive(Debug, Copy, Clone)]
enum StateRecv {
RequestLine,
Headers(Option<BodyState>),
Body(BodyState),
ValidateDone,
}
#[derive(Debug, Copy, Clone)]
enum StateSend {
TODO,
}
#[derive(Debug, Copy, Clone)]
enum StateConnection {
Recv(StateRecv),
Send(StateSend),
}
#[derive(Debug)]
pub struct Connection {
role: Role,
state: StateConnection,
}
impl Connection {
pub fn new(role: Role) -> Self {
let state = match role {
Role::Server => StateConnection::Recv(StateRecv::RequestLine),
Role::Client => StateConnection::Send(StateSend::TODO),
};
Self { state, role }
}
pub fn is_sending(&self) -> bool {
matches!(self.state, StateConnection::Send(_))
}
pub fn is_recving(&self) -> bool {
matches!(self.state, StateConnection::Recv(_))
}
pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Eat<Event<'a>> {
let recv = match &self.state {
StateConnection::Recv(r) => r,
_ => {
return Eat::Consumed {
amt: 0,
result: Err(Error::InvalidConnectionState),
}
},
};
let n = match recv {
StateRecv::RequestLine => parse::request_line(bytes).map(|rl| {
(
Event::RequestLine(rl),
StateConnection::Recv(StateRecv::Headers(None)),
)
}),
StateRecv::Headers(body_state) => {
if bytes.starts_with(b"\r\n") {
Eat::Consumed {
amt: 2,
result: Ok((
Event::HeadersEnd,
match body_state {
None => StateConnection::Send(StateSend::TODO),
Some(b) => {
StateConnection::Recv(StateRecv::Body(*b))
},
},
)),
}
} else {
parse::header(bytes).map(|h| {
let b = match h {
Header::Special(
HeaderSpecial::TransferEncodingChunked,
) => Some(BodyState::Chunked),
Header::Special(HeaderSpecial::ContentLength(
c,
)) => Some(BodyState::ContentLength(c)),
_ => *body_state,
};
(
Event::Header(h),
StateConnection::Recv(StateRecv::Headers(b)),
)
})
}
},
StateRecv::Body(body_state) => match body_state {
BodyState::ContentLength(remaining) => {
if bytes.len() < *remaining {
Eat::Consumed {
amt: bytes.len(),
result: Ok((
Event::BodyChunk(bytes),
StateConnection::Recv(StateRecv::Body(
BodyState::ContentLength(
remaining - bytes.len(),
),
)),
)),
}
} else {
Eat::Consumed {
amt: *remaining,
result: Ok((
Event::BodyChunk(bytes),
StateConnection::Recv(StateRecv::ValidateDone),
)),
}
}
},
_ => todo!(),
},
StateRecv::ValidateDone => {
if bytes.is_empty() {
Eat::Consumed {
amt: 0,
result: Ok((
Event::Done,
StateConnection::Send(StateSend::TODO),
)),
}
} else {
return Eat::Consumed {
amt: 0,
result: Err(Error::TrailingBytes),
};
}
},
};
n.map(|(ev, next_state)| {
self.state = next_state;
ev
})
}
}

133
crates/uhttp/src/parse.rs Normal file
View file

@ -0,0 +1,133 @@
use crate::{
common::Eat,
parts::{RequestLine, Version},
Error, Header, HeaderOther, HeaderSpecial,
};
pub type Parse<T> = Eat<T>;
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], usize)> {
let p = d.windows(2).position(|w| w == b"\r\n")?;
Some((&d[..p], p + 2))
}
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
let (line, amt) = match split_crlf(d) {
Some(l) => l,
None => {
return Parse::NeedMoreData;
},
};
let mut it = line
.split(|b| b.is_ascii_whitespace())
.filter(|bs| !bs.is_empty());
let (method, target, version) = match (it.next(), it.next(), it.next()) {
(Some(m), Some(t), Some(v)) => (m, t, v),
_ => {
return Parse::Consumed {
amt,
result: Err(Error::Parse(
"request line doesn't have required number of elements",
)),
}
},
};
let method = match core::str::from_utf8(method) {
Ok(m) => m,
_ => {
return Parse::Consumed {
amt,
result: Err(Error::Parse("expected method to be ascii")),
}
},
};
let target = match core::str::from_utf8(target) {
Ok(m) => m,
_ => {
return Parse::Consumed {
amt,
result: Err(Error::Parse("expected target to be ascii")),
}
},
};
let version = match () {
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
_ => {
return Parse::Consumed {
amt,
result: Err(Error::Parse("unknown http version")),
}
},
};
Parse::Consumed {
amt,
result: Ok(RequestLine {
method,
target,
version,
}),
}
}
pub fn header(d: &[u8]) -> Parse<Header> {
let (line, amt) = match split_crlf(d) {
Some(l) => l,
None => {
return Parse::NeedMoreData;
},
};
let mut it = line
.split(|b| b.is_ascii_whitespace())
.filter(|bs| !bs.is_empty());
let (name, value) = match (it.next(), it.next()) {
(Some(n), Some(v)) => (n, v),
_ => {
return Parse::Consumed {
amt,
result: Err(Error::Parse(
"header doesn't have required number of elements",
)),
}
},
};
let name = match core::str::from_utf8(name) {
Ok(m) => m,
_ => {
return Parse::Consumed {
amt,
result: Err(Error::Parse("expected target to be ascii")),
}
},
};
let name = name.split_once(":").map(|(f, _)| f).unwrap_or(name);
let h = match () {
_ if name.eq_ignore_ascii_case("transfer-encoding")
&& value.eq_ignore_ascii_case(b"chunked") =>
{
Header::Special(HeaderSpecial::TransferEncodingChunked)
},
_ if name.eq_ignore_ascii_case("content-length") => {
match core::str::from_utf8(value)
.ok()
.and_then(|s| s.parse().ok())
{
Some(v) => Header::Special(HeaderSpecial::ContentLength(v)),
_ => Header::Other(HeaderOther { name, value }),
}
},
_ => Header::Other(HeaderOther { name, value }),
};
Parse::Consumed { amt, result: Ok(h) }
}

30
crates/uhttp/src/parts.rs Normal file
View file

@ -0,0 +1,30 @@
#[derive(Debug)]
pub enum Version {
HTTP1_1,
}
#[derive(Debug)]
pub struct RequestLine<'a> {
pub method: &'a str,
pub target: &'a str,
pub version: Version,
}
/// Headers that impact the state of the connection
#[derive(Debug)]
pub enum HeaderSpecial {
TransferEncodingChunked,
ContentLength(usize),
}
#[derive(Debug)]
pub struct HeaderOther<'a> {
pub name: &'a str,
pub value: &'a [u8],
}
#[derive(Debug)]
pub enum Header<'a> {
Special(HeaderSpecial),
Other(HeaderOther<'a>),
}

165
flake.lock Normal file
View file

@ -0,0 +1,165 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1728344376,
"narHash": "sha256-lxTce2XE6mfJH8Zk6yBbqsbu9/jpwdymbSH5cCbiVOA=",
"owner": "ipetkov",
"repo": "crane",
"rev": "fd86b78f5f35f712c72147427b1eb81a9bd55d0b",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"deno-flake": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 0,
"narHash": "sha256-RsFYFS4k28JneAcEQO8ITXqIXKyBIa8FIKEPfz3NJHA=",
"type": "git",
"url": "file:///home/n/src/deno-flake"
},
"original": {
"type": "git",
"url": "file:///home/n/src/deno-flake"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1728455642,
"narHash": "sha256-abYGwrL6ak5sBRqwPh+V3CPJ6Pa89p378t51b7BO1lE=",
"owner": "nix-community",
"repo": "fenix",
"rev": "3b47535a5c782e4f4ad59cd4bdb23636b6926e03",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1725930920,
"narHash": "sha256-RVhD9hnlTT2nJzPHlAqrWqCkA7T6CYrP41IoVRkciZM=",
"path": "/nix/store/20yis5w6g397plssim663hqxdiiah2wr-source",
"rev": "44a71ff39c182edaf25a7ace5c9454e7cba2c658",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"crane": "crane",
"deno-flake": "deno-flake",
"fenix": "fenix",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1728386838,
"narHash": "sha256-Lk64EoJkvp3WMGVJK3CR1TYcNghX0/BqHPLW5zdvmLE=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "efaf8bd5de34e2f47bd57425b83e0c7974902176",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

46
flake.nix Normal file
View file

@ -0,0 +1,46 @@
{
inputs.nixpkgs.url = "nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.deno-flake = {
url = "git+file:///home/n/src/deno-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
inputs.fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
inputs.crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = i:
i.flake-utils.lib.eachDefaultSystem (system:
let
pkgs = i.nixpkgs.legacyPackages.${system};
pkgsDeno = i.deno-flake.packages.${system};
pkgsFenix = i.fenix.packages.${system};
nightly = pkgsFenix.default;
stable = pkgsFenix.stable;
in {
devShells.default = pkgs.mkShell {
packages = [
pkgsDeno.deno-latest
(pkgs.sqlite.override { interactive = true; })
pkgs.unzip
pkgs.entr
pkgs.gnumake
pkgs.fd
(pkgsFenix.combine [
(stable.withComponents [
"cargo" "rustc" "rust-src" "rust-analyzer" "clippy"
])
nightly.rustfmt
])
pkgs.cargo-watch
];
};
});
}

5
rustfmt.toml Normal file
View file

@ -0,0 +1,5 @@
edition = "2021"
hard_tabs = true
match_block_trailing_comma = true
max_width = 80
empty_item_single_line = false

34
src/main.rs Normal file
View file

@ -0,0 +1,34 @@
use std::{error::Error, io::Read, net::TcpListener};
fn main() -> Result<(), Box<dyn Error>> {
let listener = TcpListener::bind("127.0.0.1:8089")?;
loop {
let (mut stream, _) = listener.accept()?;
let mut buf = vec![0; 1024];
let mut conn = uhttp::Connection::new(uhttp::Role::Server);
let end = stream.read(&mut buf)?;
let mut data = &buf[..end];
loop {
dbg!(&conn, data);
match conn.handle_recv(data) {
uhttp::Eat::NeedMoreData => todo!(),
uhttp::Eat::Consumed { amt, result } => match result {
Ok(ev) => match ev {
uhttp::Event::Done => break,
_ => {
dbg!(ev);
data = &data[amt..];
},
},
Err(e) => {
dbg!(e);
},
},
}
}
}
}