[uhttp] echo server example
This commit is contained in:
parent
fd85c12da3
commit
f548be3ea3
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -7,8 +7,19 @@ name = "shelves"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"uhttp",
|
"uhttp",
|
||||||
|
"uhttp-ext",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uhttp"
|
name = "uhttp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"uhttp-ext",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uhttp-ext"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"uhttp",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/uhttp"
|
"crates/uhttp",
|
||||||
|
"crates/uhttp/crates/uhttp-ext",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
@ -11,3 +12,4 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uhttp = { path = "crates/uhttp" }
|
uhttp = { path = "crates/uhttp" }
|
||||||
|
uhttp-ext = { path = "crates/uhttp/crates/uhttp-ext" }
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,6 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
uhttp-ext = { path = "./crates/uhttp-ext" }
|
||||||
7
crates/uhttp/crates/uhttp-ext/Cargo.toml
Normal file
7
crates/uhttp/crates/uhttp-ext/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "uhttp-ext"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
uhttp = { path = "../.." }
|
||||||
99
crates/uhttp/crates/uhttp-ext/src/lib.rs
Normal file
99
crates/uhttp/crates/uhttp-ext/src/lib.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use std::io::{Read, Result as IOResult};
|
||||||
|
|
||||||
|
pub struct NotEnoughSpace;
|
||||||
|
pub struct Buf<T, U> {
|
||||||
|
written_len: usize,
|
||||||
|
data: T,
|
||||||
|
_dt: core::marker::PhantomData<U>,
|
||||||
|
}
|
||||||
|
impl<T, U> Buf<T, U> {
|
||||||
|
pub fn new(data: T) -> Self {
|
||||||
|
Self {
|
||||||
|
data,
|
||||||
|
written_len: 0,
|
||||||
|
_dt: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> Buf<T, U>
|
||||||
|
where
|
||||||
|
T: AsRef<[U]>,
|
||||||
|
{
|
||||||
|
pub fn remaining(&self) -> &[U] {
|
||||||
|
&self.data.as_ref()[self.written_len..]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filled(&self) -> &[U] {
|
||||||
|
&self.data.as_ref()[..self.written_len]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> Buf<T, U>
|
||||||
|
where
|
||||||
|
T: AsRef<[U]>,
|
||||||
|
T: AsMut<[U]>,
|
||||||
|
{
|
||||||
|
pub fn remaining_mut(&mut self) -> &mut [U] {
|
||||||
|
&mut self.data.as_mut()[self.written_len..]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_from_slice(&mut self, s: &[U]) -> Result<(), NotEnoughSpace>
|
||||||
|
where
|
||||||
|
U: Copy,
|
||||||
|
{
|
||||||
|
if self.remaining().len() < s.len() {
|
||||||
|
return Err(NotEnoughSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.remaining_mut()[..s.len()].copy_from_slice(s);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_front(&mut self, amt: usize)
|
||||||
|
where
|
||||||
|
U: Copy,
|
||||||
|
{
|
||||||
|
if amt > self.filled().len() {
|
||||||
|
panic!("Filled not big enough");
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = self.data.as_mut();
|
||||||
|
|
||||||
|
let src = &data[amt..];
|
||||||
|
let count = data.len() - amt;
|
||||||
|
|
||||||
|
// SAFETY:
|
||||||
|
// - src comes from data
|
||||||
|
// - U is copy
|
||||||
|
// - count is within bounds of data
|
||||||
|
unsafe { core::ptr::copy(src.as_ptr(), data.as_mut_ptr(), count) }
|
||||||
|
|
||||||
|
self.written_len -= amt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Buf<T, u8>
|
||||||
|
where
|
||||||
|
T: AsRef<[u8]> + AsMut<[u8]>,
|
||||||
|
{
|
||||||
|
pub fn read_from(&mut self, mut r: impl Read) -> IOResult<usize> {
|
||||||
|
let remaining = self.remaining_mut();
|
||||||
|
let amt = r.read(remaining)?;
|
||||||
|
|
||||||
|
self.written_len += amt;
|
||||||
|
|
||||||
|
Ok(amt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> uhttp::Write for Buf<T, u8>
|
||||||
|
where
|
||||||
|
T: AsRef<[u8]> + AsMut<[u8]>,
|
||||||
|
{
|
||||||
|
fn write(&mut self, buf: &[u8]) -> uhttp::Written {
|
||||||
|
self.extend_from_slice(buf)
|
||||||
|
.map_err(|_| uhttp::ErrorKind::BufNotBigEnough.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
82
crates/uhttp/examples/echo.rs
Normal file
82
crates/uhttp/examples/echo.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
//! A simple echo server that listens on 127.0.0.1:8089
|
||||||
|
|
||||||
|
use std::{error::Error, io::Write, net::TcpListener};
|
||||||
|
|
||||||
|
use uhttp::Lift;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:8089")?;
|
||||||
|
loop {
|
||||||
|
let (mut stream, _) = listener.accept()?;
|
||||||
|
let mut conn = uhttp::Connection::new(uhttp::Role::Server);
|
||||||
|
|
||||||
|
let mut body: Vec<u8> = Vec::new();
|
||||||
|
let mut buf = vec![0; 1024].into_boxed_slice();
|
||||||
|
let mut buf = uhttp_ext::Buf::new(&mut buf);
|
||||||
|
|
||||||
|
let mut method_not_allowed = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let data = buf.filled();
|
||||||
|
let (remaining, r) = conn.handle_recv(data).lift();
|
||||||
|
match r.map_err(|e| e.kind) {
|
||||||
|
Err(uhttp::ErrorKind::NeedMoreData) => {
|
||||||
|
buf.read_from(&mut stream)?;
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
Err(e) => panic!("{e:?}"),
|
||||||
|
Ok(event) => {
|
||||||
|
match event {
|
||||||
|
uhttp::Event::RequestLine(r) => {
|
||||||
|
if !r.method.eq_ignore_ascii_case("post") {
|
||||||
|
method_not_allowed = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uhttp::Event::RecvDone => break,
|
||||||
|
uhttp::Event::BodyChunk(b) => body.extend_from_slice(b),
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let len = data.len() - remaining.len();
|
||||||
|
buf.pop_front(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts: &[uhttp::Event] = if method_not_allowed {
|
||||||
|
&[
|
||||||
|
uhttp::Event::StatusLine(uhttp::StatusLine {
|
||||||
|
version: uhttp::Version::HTTP1_1,
|
||||||
|
status_code: 405,
|
||||||
|
status_text: "Method not allowed",
|
||||||
|
}),
|
||||||
|
uhttp::Event::HeadersEnd,
|
||||||
|
uhttp::Event::SendDone,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
&[
|
||||||
|
uhttp::Event::StatusLine(uhttp::StatusLine {
|
||||||
|
version: uhttp::Version::HTTP1_1,
|
||||||
|
status_code: 200,
|
||||||
|
status_text: "OK",
|
||||||
|
}),
|
||||||
|
uhttp::Event::Header(uhttp::Header::Special(
|
||||||
|
uhttp::HeaderSpecial::ContentLength(body.len()),
|
||||||
|
)),
|
||||||
|
uhttp::Event::HeadersEnd,
|
||||||
|
uhttp::Event::BodyChunk(body.as_slice()),
|
||||||
|
uhttp::Event::SendDone,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let buf = vec![0; 1024];
|
||||||
|
let mut cursor = uhttp::WriteCursor::new(buf);
|
||||||
|
for p in parts {
|
||||||
|
if let Err(e) = conn.handle_send(p, &mut cursor) {
|
||||||
|
panic!("{e:?}")
|
||||||
|
};
|
||||||
|
stream.write_all(cursor.written())?;
|
||||||
|
cursor.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,41 +1,38 @@
|
||||||
use crate::Error;
|
#[derive(Debug)]
|
||||||
|
pub enum ErrorKind {
|
||||||
pub enum Eat<T> {
|
|
||||||
NeedMoreData,
|
NeedMoreData,
|
||||||
Consumed {
|
InvalidConnectionState,
|
||||||
amt: usize,
|
Parse,
|
||||||
result: Result<T, Error>,
|
TrailingBytes,
|
||||||
},
|
BufNotBigEnough,
|
||||||
|
InvalidEventForConnectionState,
|
||||||
|
BodySizeMismatch,
|
||||||
}
|
}
|
||||||
impl<T> Eat<T> {
|
|
||||||
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Eat<U> {
|
#[derive(Debug)]
|
||||||
match self {
|
pub struct Error {
|
||||||
Eat::NeedMoreData => Eat::NeedMoreData,
|
pub kind: ErrorKind,
|
||||||
Eat::Consumed {
|
pub details: &'static str,
|
||||||
amt,
|
}
|
||||||
result: Err(e),
|
impl Error {
|
||||||
} => Eat::Consumed {
|
pub fn new(kind: ErrorKind) -> Self {
|
||||||
amt,
|
Self::with_details(kind, "")
|
||||||
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> {
|
pub fn with_details(kind: ErrorKind, details: &'static str) -> Self {
|
||||||
match self {
|
Error { kind, details }
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<ErrorKind> for Error {
|
||||||
|
fn from(value: ErrorKind) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail<T>(error_kind: ErrorKind) -> Result<T, Error> {
|
||||||
|
fail_details(error_kind, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail_details<T>(error_kind: ErrorKind, details: &'static str) -> Result<T, Error> {
|
||||||
|
Err(Error::with_details(error_kind, details))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,16 @@
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod parse;
|
pub mod parse;
|
||||||
pub mod parts;
|
pub mod parts;
|
||||||
|
pub mod util;
|
||||||
|
pub mod write;
|
||||||
|
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
pub use parse::Parse;
|
||||||
pub use parts::*;
|
pub use parts::*;
|
||||||
|
pub use util::*;
|
||||||
|
pub use write::{Write, WriteCursor, Written};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub enum Error {
|
|
||||||
InvalidConnectionState,
|
|
||||||
Parse(&'static str),
|
|
||||||
TrailingBytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub enum Event<'a> {
|
pub enum Event<'a> {
|
||||||
#[default]
|
#[default]
|
||||||
Empty,
|
Empty,
|
||||||
|
|
@ -21,7 +19,9 @@ pub enum Event<'a> {
|
||||||
Header(Header<'a>),
|
Header(Header<'a>),
|
||||||
HeadersEnd,
|
HeadersEnd,
|
||||||
BodyChunk(&'a [u8]),
|
BodyChunk(&'a [u8]),
|
||||||
Done,
|
RecvDone,
|
||||||
|
StatusLine(StatusLine<'a>),
|
||||||
|
SendDone,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -46,7 +46,10 @@ enum StateRecv {
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
enum StateSend {
|
enum StateSend {
|
||||||
TODO,
|
StatusLine,
|
||||||
|
Headers(Option<BodyState>),
|
||||||
|
Body(BodyState),
|
||||||
|
ValidateDone,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
|
@ -64,7 +67,7 @@ impl Connection {
|
||||||
pub fn new(role: Role) -> Self {
|
pub fn new(role: Role) -> Self {
|
||||||
let state = match role {
|
let state = match role {
|
||||||
Role::Server => StateConnection::Recv(StateRecv::RequestLine),
|
Role::Server => StateConnection::Recv(StateRecv::RequestLine),
|
||||||
Role::Client => StateConnection::Send(StateSend::TODO),
|
Role::Client => StateConnection::Send(StateSend::StatusLine),
|
||||||
};
|
};
|
||||||
|
|
||||||
Self { state, role }
|
Self { state, role }
|
||||||
|
|
@ -78,47 +81,43 @@ impl Connection {
|
||||||
matches!(self.state, StateConnection::Recv(_))
|
matches!(self.state, StateConnection::Recv(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Eat<Event<'a>> {
|
pub fn handle_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> {
|
||||||
let recv = match &self.state {
|
let recv = match &self.state {
|
||||||
StateConnection::Recv(r) => r,
|
StateConnection::Recv(r) => r,
|
||||||
_ => {
|
_ => {
|
||||||
return Eat::Consumed {
|
return Err((bytes, ErrorKind::InvalidConnectionState.into()));
|
||||||
amt: 0,
|
|
||||||
result: Err(Error::InvalidConnectionState),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let n = match recv {
|
let n = match recv {
|
||||||
StateRecv::RequestLine => parse::request_line(bytes).map(|rl| {
|
StateRecv::RequestLine => parse::request_line(bytes).map2(|rl| {
|
||||||
(
|
(
|
||||||
Event::RequestLine(rl),
|
Event::RequestLine(rl),
|
||||||
StateConnection::Recv(StateRecv::Headers(None)),
|
StateConnection::Recv(StateRecv::Headers(None)),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
StateRecv::Headers(body_state) => {
|
StateRecv::Headers(body_state) => {
|
||||||
if bytes.starts_with(b"\r\n") {
|
if bytes.starts_with(b"\r\n") {
|
||||||
Eat::Consumed {
|
Ok((
|
||||||
amt: 2,
|
&bytes[2..],
|
||||||
result: Ok((
|
(
|
||||||
Event::HeadersEnd,
|
Event::HeadersEnd,
|
||||||
match body_state {
|
match body_state {
|
||||||
None => StateConnection::Send(StateSend::TODO),
|
None => StateConnection::Recv(StateRecv::ValidateDone),
|
||||||
Some(b) => {
|
Some(b) => StateConnection::Recv(StateRecv::Body(*b)),
|
||||||
StateConnection::Recv(StateRecv::Body(*b))
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
}
|
))
|
||||||
} else {
|
} else {
|
||||||
parse::header(bytes).map(|h| {
|
parse::header(bytes).map2(|h| {
|
||||||
let b = match h {
|
let b = match h {
|
||||||
Header::Special(
|
Header::Special(HeaderSpecial::TransferEncodingChunked) => {
|
||||||
HeaderSpecial::TransferEncodingChunked,
|
Some(BodyState::Chunked)
|
||||||
) => Some(BodyState::Chunked),
|
},
|
||||||
Header::Special(HeaderSpecial::ContentLength(
|
Header::Special(HeaderSpecial::ContentLength(c)) => {
|
||||||
c,
|
Some(BodyState::ContentLength(c))
|
||||||
)) => Some(BodyState::ContentLength(c)),
|
},
|
||||||
_ => *body_state,
|
_ => *body_state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -129,54 +128,120 @@ impl Connection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
StateRecv::Body(body_state) => match body_state {
|
StateRecv::Body(body_state) => match body_state {
|
||||||
BodyState::ContentLength(remaining) => {
|
BodyState::ContentLength(remaining) => {
|
||||||
if bytes.len() < *remaining {
|
if bytes.len() < *remaining {
|
||||||
Eat::Consumed {
|
Ok((
|
||||||
amt: bytes.len(),
|
&[] as &[u8],
|
||||||
result: Ok((
|
(
|
||||||
Event::BodyChunk(bytes),
|
Event::BodyChunk(bytes),
|
||||||
StateConnection::Recv(StateRecv::Body(
|
StateConnection::Recv(StateRecv::Body(
|
||||||
BodyState::ContentLength(
|
BodyState::ContentLength(remaining - bytes.len()),
|
||||||
remaining - bytes.len(),
|
|
||||||
),
|
|
||||||
)),
|
)),
|
||||||
)),
|
),
|
||||||
}
|
))
|
||||||
} else {
|
} else {
|
||||||
Eat::Consumed {
|
Ok((
|
||||||
amt: *remaining,
|
&bytes[*remaining..],
|
||||||
result: Ok((
|
(
|
||||||
Event::BodyChunk(bytes),
|
Event::BodyChunk(&bytes[..*remaining]),
|
||||||
StateConnection::Recv(StateRecv::ValidateDone),
|
StateConnection::Recv(StateRecv::ValidateDone),
|
||||||
)),
|
),
|
||||||
}
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
},
|
},
|
||||||
|
|
||||||
StateRecv::ValidateDone => {
|
StateRecv::ValidateDone => {
|
||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
Eat::Consumed {
|
Ok((
|
||||||
amt: 0,
|
bytes,
|
||||||
result: Ok((
|
(
|
||||||
Event::Done,
|
Event::RecvDone,
|
||||||
StateConnection::Send(StateSend::TODO),
|
StateConnection::Send(StateSend::StatusLine),
|
||||||
)),
|
),
|
||||||
}
|
))
|
||||||
} else {
|
} else {
|
||||||
return Eat::Consumed {
|
fail(ErrorKind::TrailingBytes).tup(bytes)
|
||||||
amt: 0,
|
|
||||||
result: Err(Error::TrailingBytes),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
n.map(|(ev, next_state)| {
|
n.map2(move |(ev, next_state)| {
|
||||||
self.state = next_state;
|
self.state = next_state;
|
||||||
|
|
||||||
ev
|
ev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_send(&mut self, event: &Event, mut w: impl Write) -> Written {
|
||||||
|
let state = match self.state {
|
||||||
|
StateConnection::Send(s) => s,
|
||||||
|
_ => return fail(ErrorKind::InvalidConnectionState),
|
||||||
|
};
|
||||||
|
|
||||||
|
let next = match (state, event) {
|
||||||
|
(StateSend::StatusLine, Event::StatusLine(sl)) => write::status_line(sl, w)
|
||||||
|
.map(|_| StateConnection::Send(StateSend::Headers(None))),
|
||||||
|
|
||||||
|
(StateSend::Headers(body_state), Event::Header(h)) => {
|
||||||
|
write::header(h, w)?;
|
||||||
|
|
||||||
|
let bs = match h {
|
||||||
|
Header::Other(_) => body_state,
|
||||||
|
Header::Special(h) => match h {
|
||||||
|
HeaderSpecial::TransferEncodingChunked => Some(BodyState::Chunked),
|
||||||
|
HeaderSpecial::ContentLength(cl) => {
|
||||||
|
Some(BodyState::ContentLength(*cl))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(StateConnection::Send(StateSend::Headers(bs)))
|
||||||
|
},
|
||||||
|
|
||||||
|
(StateSend::Headers(body_state), Event::HeadersEnd) => {
|
||||||
|
write!(w, "\r\n")?;
|
||||||
|
match body_state {
|
||||||
|
Some(bs) => Ok(StateConnection::Send(StateSend::Body(bs))),
|
||||||
|
None => Ok(StateConnection::Send(StateSend::ValidateDone)),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
(StateSend::Body(b), Event::BodyChunk(c)) => match b {
|
||||||
|
BodyState::ContentLength(cl) => match () {
|
||||||
|
_ if c.len() < cl => {
|
||||||
|
w.write(c)?;
|
||||||
|
|
||||||
|
Ok(StateConnection::Send(StateSend::Body(
|
||||||
|
BodyState::ContentLength(cl - c.len()),
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
|
||||||
|
_ if c.len() == cl => {
|
||||||
|
w.write(c)?;
|
||||||
|
|
||||||
|
Ok(StateConnection::Send(StateSend::ValidateDone))
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
return fail(ErrorKind::BodySizeMismatch);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BodyState::Chunked => todo!(),
|
||||||
|
},
|
||||||
|
|
||||||
|
(StateSend::ValidateDone, Event::SendDone) => {
|
||||||
|
Ok(StateConnection::Recv(StateRecv::RequestLine))
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
self.state = next;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,133 +1,113 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
common::Eat,
|
fail_details,
|
||||||
parts::{RequestLine, Version},
|
parts::{RequestLine, Version},
|
||||||
Error, Header, HeaderOther, HeaderSpecial,
|
Error, ErrorKind, Header, HeaderOther, HeaderSpecial, Tup,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Parse<T> = Eat<T>;
|
pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>;
|
||||||
|
|
||||||
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], usize)> {
|
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||||
let p = d.windows(2).position(|w| w == b"\r\n")?;
|
let p = d.windows(2).position(|w| w == b"\r\n")?;
|
||||||
|
|
||||||
Some((&d[..p], p + 2))
|
Some((&d[..p], &d[p + 2..]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
|
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
|
||||||
let (line, amt) = match split_crlf(d) {
|
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
return Parse::NeedMoreData;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut it = line
|
let go = || {
|
||||||
.split(|b| b.is_ascii_whitespace())
|
let mut it = line
|
||||||
.filter(|bs| !bs.is_empty());
|
.split(|b| b.is_ascii_whitespace())
|
||||||
|
.filter(|bs| !bs.is_empty());
|
||||||
|
|
||||||
let (method, target, version) = match (it.next(), it.next(), it.next()) {
|
let (method, target, version) = match (it.next(), it.next(), it.next()) {
|
||||||
(Some(m), Some(t), Some(v)) => (m, t, v),
|
(Some(m), Some(t), Some(v)) => (m, t, v),
|
||||||
_ => {
|
_ => {
|
||||||
return Parse::Consumed {
|
return fail_details(
|
||||||
amt,
|
ErrorKind::Parse,
|
||||||
result: Err(Error::Parse(
|
|
||||||
"request line doesn't have required number of elements",
|
"request line doesn't have required number of elements",
|
||||||
)),
|
);
|
||||||
}
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let method = match core::str::from_utf8(method) {
|
let method = match core::str::from_utf8(method) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
_ => {
|
_ => {
|
||||||
return Parse::Consumed {
|
return fail_details(ErrorKind::Parse, "expected method to be ascii");
|
||||||
amt,
|
},
|
||||||
result: Err(Error::Parse("expected method to be ascii")),
|
};
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = match core::str::from_utf8(target) {
|
let target = match core::str::from_utf8(target) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
_ => {
|
_ => {
|
||||||
return Parse::Consumed {
|
return fail_details(ErrorKind::Parse, "expected target to be ascii");
|
||||||
amt,
|
},
|
||||||
result: Err(Error::Parse("expected target to be ascii")),
|
};
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let version = match () {
|
let version = match () {
|
||||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
||||||
_ => {
|
_ => {
|
||||||
return Parse::Consumed {
|
return fail_details(ErrorKind::Parse, "unknown http version");
|
||||||
amt,
|
},
|
||||||
result: Err(Error::Parse("unknown http version")),
|
};
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Parse::Consumed {
|
Ok(RequestLine {
|
||||||
amt,
|
|
||||||
result: Ok(RequestLine {
|
|
||||||
method,
|
method,
|
||||||
target,
|
target,
|
||||||
version,
|
version,
|
||||||
}),
|
})
|
||||||
}
|
};
|
||||||
|
|
||||||
|
go().tup(rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(d: &[u8]) -> Parse<Header> {
|
pub fn header(d: &[u8]) -> Parse<Header> {
|
||||||
let (line, amt) = match split_crlf(d) {
|
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
return Parse::NeedMoreData;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut it = line
|
let go = || {
|
||||||
.split(|b| b.is_ascii_whitespace())
|
let mut it = line
|
||||||
.filter(|bs| !bs.is_empty());
|
.split(|b| b.is_ascii_whitespace())
|
||||||
|
.filter(|bs| !bs.is_empty());
|
||||||
|
|
||||||
let (name, value) = match (it.next(), it.next()) {
|
let (name, value) = match (it.next(), it.next()) {
|
||||||
(Some(n), Some(v)) => (n, v),
|
(Some(n), Some(v)) => (n, v),
|
||||||
_ => {
|
_ => {
|
||||||
return Parse::Consumed {
|
return fail_details(
|
||||||
amt,
|
ErrorKind::Parse,
|
||||||
result: Err(Error::Parse(
|
|
||||||
"header doesn't have required number of elements",
|
"header doesn't have required number of elements",
|
||||||
)),
|
);
|
||||||
}
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let name = match core::str::from_utf8(name) {
|
let name = match core::str::from_utf8(name) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
_ => {
|
_ => return fail_details(ErrorKind::Parse, "expected target to be ascii"),
|
||||||
return Parse::Consumed {
|
};
|
||||||
amt,
|
let name = name
|
||||||
result: Err(Error::Parse("expected target to be ascii")),
|
.split_once(":")
|
||||||
}
|
.map(|(f, _)| f)
|
||||||
},
|
.ok_or(Error::with_details(ErrorKind::Parse, "invalid header name"))?;
|
||||||
};
|
|
||||||
let name = name.split_once(":").map(|(f, _)| f).unwrap_or(name);
|
|
||||||
|
|
||||||
let h = match () {
|
let h = match () {
|
||||||
_ if name.eq_ignore_ascii_case("transfer-encoding")
|
_ if name.eq_ignore_ascii_case("transfer-encoding")
|
||||||
&& value.eq_ignore_ascii_case(b"chunked") =>
|
&& 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::Special(HeaderSpecial::TransferEncodingChunked)
|
||||||
_ => Header::Other(HeaderOther { name, value }),
|
},
|
||||||
}
|
_ if name.eq_ignore_ascii_case("content-length") => {
|
||||||
},
|
match core::str::from_utf8(value)
|
||||||
_ => Header::Other(HeaderOther { name, 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 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(h)
|
||||||
};
|
};
|
||||||
|
|
||||||
Parse::Consumed { amt, result: Ok(h) }
|
go().tup(rest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,43 @@
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub enum Version {
|
pub enum Version {
|
||||||
HTTP1_1,
|
HTTP1_1,
|
||||||
}
|
}
|
||||||
|
impl core::fmt::Display for Version {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Version::HTTP1_1 => write!(f, "HTTP/1.1"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub struct RequestLine<'a> {
|
pub struct RequestLine<'a> {
|
||||||
pub method: &'a str,
|
pub method: &'a str,
|
||||||
pub target: &'a str,
|
pub target: &'a str,
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub struct StatusLine<'a> {
|
||||||
|
pub version: Version,
|
||||||
|
pub status_code: u16,
|
||||||
|
pub status_text: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
/// Headers that impact the state of the connection
|
/// Headers that impact the state of the connection
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub enum HeaderSpecial {
|
pub enum HeaderSpecial {
|
||||||
TransferEncodingChunked,
|
TransferEncodingChunked,
|
||||||
ContentLength(usize),
|
ContentLength(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub struct HeaderOther<'a> {
|
pub struct HeaderOther<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub value: &'a [u8],
|
pub value: &'a [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub enum Header<'a> {
|
pub enum Header<'a> {
|
||||||
Special(HeaderSpecial),
|
Special(HeaderSpecial),
|
||||||
Other(HeaderOther<'a>),
|
Other(HeaderOther<'a>),
|
||||||
|
|
|
||||||
37
crates/uhttp/src/util.rs
Normal file
37
crates/uhttp/src/util.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::{ErrorKind, Write};
|
||||||
|
|
||||||
|
pub trait ResultTupExt<A, T, B, E> {
|
||||||
|
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>;
|
||||||
|
}
|
||||||
|
impl<A, T, B, E> ResultTupExt<A, T, B, E> for Result<(A, T), (B, E)> {
|
||||||
|
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)> {
|
||||||
|
match self {
|
||||||
|
Ok((a, t)) => Ok((a, f(t))),
|
||||||
|
Err((b, e)) => Err((b, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Tup<T, E> {
|
||||||
|
fn tup<V>(self, v: V) -> Result<(V, T), (V, E)>;
|
||||||
|
}
|
||||||
|
impl<T, E> Tup<T, E> for Result<T, E> {
|
||||||
|
fn tup<V>(self, v: V) -> Result<(V, T), (V, E)> {
|
||||||
|
match self {
|
||||||
|
Ok(t) => Ok((v, t)),
|
||||||
|
Err(e) => Err((v, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Lift<V, T, E> {
|
||||||
|
fn lift(self) -> (V, Result<T, E>);
|
||||||
|
}
|
||||||
|
impl<V, T, E> Lift<V, T, E> for Result<(V, T), (V, E)> {
|
||||||
|
fn lift(self) -> (V, Result<T, E>) {
|
||||||
|
match self {
|
||||||
|
Ok((v, t)) => (v, Ok(t)),
|
||||||
|
Err((v, e)) => (v, Err(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
crates/uhttp/src/write.rs
Normal file
118
crates/uhttp/src/write.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
use core::fmt::Arguments;
|
||||||
|
|
||||||
|
use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, StatusLine};
|
||||||
|
|
||||||
|
pub struct FmtWriteAdapter<T> {
|
||||||
|
inner: T,
|
||||||
|
err: Written,
|
||||||
|
}
|
||||||
|
impl<T> FmtWriteAdapter<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self { inner, err: Ok(()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Write for FmtWriteAdapter<T>
|
||||||
|
where
|
||||||
|
T: Write + Sized,
|
||||||
|
{
|
||||||
|
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||||
|
self.inner.write(s.as_bytes()).map_err(|e| {
|
||||||
|
self.err = Err(e);
|
||||||
|
core::fmt::Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Write {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> Written;
|
||||||
|
|
||||||
|
fn write_fmt(&mut self, args: Arguments<'_>) -> Written
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let mut adp = FmtWriteAdapter::new(self);
|
||||||
|
let _ = core::fmt::Write::write_fmt(&mut adp, args);
|
||||||
|
|
||||||
|
adp.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Write for &mut T
|
||||||
|
where
|
||||||
|
T: Write,
|
||||||
|
{
|
||||||
|
fn write(&mut self, buf: &[u8]) -> Written {
|
||||||
|
(**self).write(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WriteCursor<T> {
|
||||||
|
at: usize,
|
||||||
|
buf: T,
|
||||||
|
}
|
||||||
|
impl<T> WriteCursor<T> {
|
||||||
|
pub fn new(buf: T) -> Self {
|
||||||
|
Self { at: 0, buf }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.at = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remaining_mut(&mut self) -> &mut [u8]
|
||||||
|
where
|
||||||
|
T: AsMut<[u8]>,
|
||||||
|
{
|
||||||
|
&mut self.buf.as_mut()[self.at..]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn written(&mut self) -> &[u8]
|
||||||
|
where
|
||||||
|
T: AsRef<[u8]>,
|
||||||
|
{
|
||||||
|
&self.buf.as_ref()[..self.at]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Write for WriteCursor<T>
|
||||||
|
where
|
||||||
|
T: AsMut<[u8]>,
|
||||||
|
{
|
||||||
|
fn write(&mut self, buf: &[u8]) -> Written {
|
||||||
|
let remaining = self.remaining_mut();
|
||||||
|
if buf.len() > remaining.len() {
|
||||||
|
return Err(Error::from(ErrorKind::BufNotBigEnough));
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining[..buf.len()].copy_from_slice(buf);
|
||||||
|
self.at += buf.len();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Written = Result<(), Error>;
|
||||||
|
|
||||||
|
pub fn status_line(sl: &StatusLine, mut w: impl Write) -> Written {
|
||||||
|
write!(
|
||||||
|
w,
|
||||||
|
"{} {} {}\r\n",
|
||||||
|
sl.version, sl.status_code, sl.status_text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(h: &Header, mut w: impl Write) -> Written {
|
||||||
|
match h {
|
||||||
|
Header::Special(h) => match h {
|
||||||
|
HeaderSpecial::TransferEncodingChunked => write!(w, "Transfer-Encoding: Chunked"),
|
||||||
|
HeaderSpecial::ContentLength(cl) => write!(w, "Content-Length: {}", cl),
|
||||||
|
},
|
||||||
|
Header::Other(HeaderOther { name, value }) => {
|
||||||
|
write!(w, "{name}: ")?;
|
||||||
|
w.write(value)
|
||||||
|
},
|
||||||
|
}?;
|
||||||
|
|
||||||
|
write!(w, "\r\n")
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
hard_tabs = true
|
hard_tabs = true
|
||||||
match_block_trailing_comma = true
|
match_block_trailing_comma = true
|
||||||
max_width = 80
|
max_width = 95
|
||||||
empty_item_single_line = false
|
empty_item_single_line = false
|
||||||
|
|
|
||||||
35
src/main.rs
35
src/main.rs
|
|
@ -1,34 +1,3 @@
|
||||||
use std::{error::Error, io::Read, net::TcpListener};
|
fn main() {
|
||||||
|
println!("hello, world");
|
||||||
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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue