[uhttp] client

This commit is contained in:
soup 2024-10-14 22:17:19 -04:00
parent f548be3ea3
commit 9d6d8b672e
No known key found for this signature in database
7 changed files with 219 additions and 34 deletions

View file

@ -47,10 +47,27 @@ where
} }
self.remaining_mut()[..s.len()].copy_from_slice(s); self.remaining_mut()[..s.len()].copy_from_slice(s);
self.written_len += s.len();
Ok(()) Ok(())
} }
pub fn clear(&mut self) {
self.written_len = 0;
}
pub fn pop_front_alloc(&mut self, amt: usize)
where
U: Copy + Default,
{
let to_copy = &self.filled()[amt..];
let buf_sz = to_copy.len();
let mut buf = vec![Default::default(); buf_sz].into_boxed_slice();
buf.copy_from_slice(to_copy);
self.clear();
let _ = self.extend_from_slice(&buf);
}
pub fn pop_front(&mut self, amt: usize) pub fn pop_front(&mut self, amt: usize)
where where
U: Copy, U: Copy,

View file

@ -0,0 +1,71 @@
//! A simple client that POSTs some data to a path
//!
//! Usage:
//! cargo run --example client -- <host> <port> <path> <data>
use std::{error::Error, io::Write, net::TcpStream};
use uhttp::Lift;
pub fn main() -> Result<(), Box<dyn Error>> {
let mut args = std::env::args().skip(1);
let (host, port, path, data) = args
.next()
.and_then(|h| Some((h, args.next()?, args.next()?, args.next()?)))
.ok_or_else(|| Box::<dyn Error>::from(String::from("Missing required argument")))?;
let mut stream = TcpStream::connect((host, port.parse()?))?;
let buf = vec![0; 4096];
let mut buf = uhttp_ext::Buf::new(buf);
let mut conn = uhttp::Connection::new(uhttp::Role::Client);
let events = &[
uhttp::Event::RequestLine(uhttp::RequestLine {
version: uhttp::Version::HTTP1_1,
method: "POST",
target: &path,
}),
uhttp::Event::Header(uhttp::Header::Special(uhttp::HeaderSpecial::ContentLength(
data.len(),
))),
uhttp::Event::HeadersDone,
uhttp::Event::BodyChunk(data.as_bytes()),
uhttp::Event::SendDone,
];
for event in events {
if let Err(e) = conn.handle_send(event, &mut buf) {
panic!("{e:?}");
};
}
stream.write_all(buf.filled())?;
buf.clear();
loop {
buf.read_from(&mut stream)?;
let data = buf.filled();
let (d, r) = conn.handle_recv(data).lift();
match r {
Err(uhttp::Error {
kind: uhttp::ErrorKind::NeedMoreData,
..
}) => {
continue;
},
Err(e) => panic!("{e:?}"),
Ok(ev) => {
println!("{ev}");
if ev == uhttp::Event::RecvDone {
break;
}
},
};
let len = buf.filled().len() - d.len();
buf.pop_front(len);
}
Ok(())
}

View file

@ -1,11 +1,21 @@
//! A simple echo server that listens on 127.0.0.1:8089 //! A simple echo server that echoes the body of a POST request, and returns a
//! 405 for any other method
//!
//! Usage:
//! cargo run --example echo -- <host> <port>
use std::{error::Error, io::Write, net::TcpListener}; use std::{error::Error, io::Write, net::TcpListener};
use uhttp::Lift; use uhttp::Lift;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let listener = TcpListener::bind("127.0.0.1:8089")?; let mut args = std::env::args().skip(1);
let (host, port) = args
.next()
.and_then(|h| Some((h, args.next()?)))
.ok_or_else(|| Box::<dyn Error>::from(String::from("Missing required argument")))?;
let listener = TcpListener::bind((host, port.parse()?))?;
loop { loop {
let (mut stream, _) = listener.accept()?; let (mut stream, _) = listener.accept()?;
let mut conn = uhttp::Connection::new(uhttp::Role::Server); let mut conn = uhttp::Connection::new(uhttp::Role::Server);
@ -50,7 +60,7 @@ fn main() -> Result<(), Box<dyn Error>> {
status_code: 405, status_code: 405,
status_text: "Method not allowed", status_text: "Method not allowed",
}), }),
uhttp::Event::HeadersEnd, uhttp::Event::HeadersDone,
uhttp::Event::SendDone, uhttp::Event::SendDone,
] ]
} else { } else {
@ -63,7 +73,7 @@ fn main() -> Result<(), Box<dyn Error>> {
uhttp::Event::Header(uhttp::Header::Special( uhttp::Event::Header(uhttp::Header::Special(
uhttp::HeaderSpecial::ContentLength(body.len()), uhttp::HeaderSpecial::ContentLength(body.len()),
)), )),
uhttp::Event::HeadersEnd, uhttp::Event::HeadersDone,
uhttp::Event::BodyChunk(body.as_slice()), uhttp::Event::BodyChunk(body.as_slice()),
uhttp::Event::SendDone, uhttp::Event::SendDone,
] ]

View file

@ -17,14 +17,44 @@ pub enum Event<'a> {
Empty, Empty,
RequestLine(RequestLine<'a>), RequestLine(RequestLine<'a>),
Header(Header<'a>), Header(Header<'a>),
HeadersEnd, HeadersDone,
BodyChunk(&'a [u8]), BodyChunk(&'a [u8]),
RecvDone, RecvDone,
StatusLine(StatusLine<'a>), StatusLine(StatusLine<'a>),
SendDone, SendDone,
} }
impl<'a> core::fmt::Display for Event<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Event::Empty | Event::HeadersDone | Event::RecvDone | Event::SendDone => Ok(()),
Event::RequestLine(r) => write!(f, "{} {} {}", r.method, r.target, r.version),
Event::StatusLine(s) => {
write!(f, "{} {} {}", s.version, s.status_code, s.status_text)
},
Event::Header(h) => match h {
Header::Other(HeaderOther { name, value }) => {
write!(
f,
"{}: {}",
name,
core::str::from_utf8(value).unwrap_or("<binary>")
)
},
Header::Special(h) => match h {
HeaderSpecial::ContentLength(cl) => write!(f, "Content-Length: {cl}"),
HeaderSpecial::TransferEncodingChunked => {
write!(f, "Transfer-Encoding: chunked")
},
},
},
Event::BodyChunk(b) => {
write!(f, "{}", core::str::from_utf8(b).unwrap_or("<binary>"))
},
}
}
}
#[derive(Debug)] #[derive(Debug, Copy, Clone)]
pub enum Role { pub enum Role {
Client, Client,
Server, Server,
@ -38,7 +68,7 @@ enum BodyState {
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
enum StateRecv { enum StateRecv {
RequestLine, StartLine,
Headers(Option<BodyState>), Headers(Option<BodyState>),
Body(BodyState), Body(BodyState),
ValidateDone, ValidateDone,
@ -46,7 +76,7 @@ enum StateRecv {
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
enum StateSend { enum StateSend {
StatusLine, StartLine,
Headers(Option<BodyState>), Headers(Option<BodyState>),
Body(BodyState), Body(BodyState),
ValidateDone, ValidateDone,
@ -58,7 +88,7 @@ enum StateConnection {
Send(StateSend), Send(StateSend),
} }
#[derive(Debug)] #[derive(Debug, Copy, Clone)]
pub struct Connection { pub struct Connection {
role: Role, role: Role,
state: StateConnection, state: StateConnection,
@ -66,8 +96,8 @@ pub struct Connection {
impl Connection { 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::StartLine),
Role::Client => StateConnection::Send(StateSend::StatusLine), Role::Client => StateConnection::Send(StateSend::StartLine),
}; };
Self { state, role } Self { state, role }
@ -89,20 +119,27 @@ impl Connection {
}, },
}; };
let n = match recv { let n = match (self.role, recv) {
StateRecv::RequestLine => parse::request_line(bytes).map2(|rl| { (Role::Server, StateRecv::StartLine) => 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) => { (Role::Client, StateRecv::StartLine) => parse::status_line(bytes).map2(|rl| {
(
Event::StatusLine(rl),
StateConnection::Recv(StateRecv::Headers(None)),
)
}),
(_, StateRecv::Headers(body_state)) => {
if bytes.starts_with(b"\r\n") { if bytes.starts_with(b"\r\n") {
Ok(( Ok((
&bytes[2..], &bytes[2..],
( (
Event::HeadersEnd, Event::HeadersDone,
match body_state { match body_state {
None => StateConnection::Recv(StateRecv::ValidateDone), None => StateConnection::Recv(StateRecv::ValidateDone),
Some(b) => StateConnection::Recv(StateRecv::Body(*b)), Some(b) => StateConnection::Recv(StateRecv::Body(*b)),
@ -129,7 +166,7 @@ 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 {
Ok(( Ok((
@ -154,14 +191,11 @@ impl Connection {
_ => todo!(), _ => todo!(),
}, },
StateRecv::ValidateDone => { (_, StateRecv::ValidateDone) => {
if bytes.is_empty() { if bytes.is_empty() {
Ok(( Ok((
bytes, bytes,
( (Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
Event::RecvDone,
StateConnection::Send(StateSend::StatusLine),
),
)) ))
} else { } else {
fail(ErrorKind::TrailingBytes).tup(bytes) fail(ErrorKind::TrailingBytes).tup(bytes)
@ -182,11 +216,18 @@ impl Connection {
_ => return fail(ErrorKind::InvalidConnectionState), _ => return fail(ErrorKind::InvalidConnectionState),
}; };
let next = match (state, event) { let next = match (self.role, state, event) {
(StateSend::StatusLine, Event::StatusLine(sl)) => write::status_line(sl, w) (Role::Server, StateSend::StartLine, Event::StatusLine(sl)) => {
.map(|_| StateConnection::Send(StateSend::Headers(None))), write::status_line(sl, w)
.map(|_| StateConnection::Send(StateSend::Headers(None)))
},
(StateSend::Headers(body_state), Event::Header(h)) => { (Role::Client, StateSend::StartLine, Event::RequestLine(rl)) => {
write::request_line(rl, w)
.map(|_| StateConnection::Send(StateSend::Headers(None)))
},
(_, StateSend::Headers(body_state), Event::Header(h)) => {
write::header(h, w)?; write::header(h, w)?;
let bs = match h { let bs = match h {
@ -202,7 +243,7 @@ impl Connection {
Ok(StateConnection::Send(StateSend::Headers(bs))) Ok(StateConnection::Send(StateSend::Headers(bs)))
}, },
(StateSend::Headers(body_state), Event::HeadersEnd) => { (_, StateSend::Headers(body_state), Event::HeadersDone) => {
write!(w, "\r\n")?; write!(w, "\r\n")?;
match body_state { match body_state {
Some(bs) => Ok(StateConnection::Send(StateSend::Body(bs))), Some(bs) => Ok(StateConnection::Send(StateSend::Body(bs))),
@ -210,7 +251,7 @@ impl Connection {
} }
}, },
(StateSend::Body(b), Event::BodyChunk(c)) => match b { (_, StateSend::Body(b), Event::BodyChunk(c)) => match b {
BodyState::ContentLength(cl) => match () { BodyState::ContentLength(cl) => match () {
_ if c.len() < cl => { _ if c.len() < cl => {
w.write(c)?; w.write(c)?;
@ -233,8 +274,8 @@ impl Connection {
BodyState::Chunked => todo!(), BodyState::Chunked => todo!(),
}, },
(StateSend::ValidateDone, Event::SendDone) => { (_, StateSend::ValidateDone, Event::SendDone) => {
Ok(StateConnection::Recv(StateRecv::RequestLine)) Ok(StateConnection::Recv(StateRecv::StartLine))
}, },
_ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)), _ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)),

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
fail_details, fail_details,
parts::{RequestLine, Version}, parts::{RequestLine, Version},
Error, ErrorKind, Header, HeaderOther, HeaderSpecial, Tup, Error, ErrorKind, Header, HeaderOther, HeaderSpecial, StatusLine, Tup,
}; };
pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>; pub type Parse<'a, T> = Result<(&'a [u8], T), (&'a [u8], Error)>;
@ -61,6 +61,50 @@ pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
go().tup(rest) go().tup(rest)
} }
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;
let go = || {
let mut it = line
.split(|b| b.is_ascii_whitespace())
.filter(|bs| !bs.is_empty());
let (version, status_code, status_text) = match (it.next(), it.next(), it.next()) {
(Some(m), Some(t), Some(v)) => (m, t, v),
_ => {
return fail_details(
ErrorKind::Parse,
"status line doesn't have required number of elements",
);
},
};
let version = match () {
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
_ => {
return fail_details(ErrorKind::Parse, "unknown http version");
},
};
let status_code = core::str::from_utf8(status_code)
.ok()
.and_then(|s| s.parse().ok())
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status code"))?;
let status_text = core::str::from_utf8(status_text)
.ok()
.ok_or_else(|| Error::with_details(ErrorKind::Parse, "invalid status text"))?;
Ok(StatusLine {
version,
status_code,
status_text,
})
};
go().tup(rest)
}
pub fn header(d: &[u8]) -> Parse<Header> { pub fn header(d: &[u8]) -> Parse<Header> {
let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?; let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?;

View file

@ -1,5 +1,3 @@
use crate::{ErrorKind, Write};
pub trait ResultTupExt<A, T, B, E> { pub trait ResultTupExt<A, T, B, E> {
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>; fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>;
} }

View file

@ -1,6 +1,6 @@
use core::fmt::Arguments; use core::fmt::Arguments;
use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, StatusLine}; use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, RequestLine, StatusLine};
pub struct FmtWriteAdapter<T> { pub struct FmtWriteAdapter<T> {
inner: T, inner: T,
@ -102,6 +102,10 @@ pub fn status_line(sl: &StatusLine, mut w: impl Write) -> Written {
) )
} }
pub fn request_line(rl: &RequestLine, mut w: impl Write) -> Written {
write!(w, "{} {} {}\r\n", rl.method, rl.target, rl.version)
}
pub fn header(h: &Header, mut w: impl Write) -> Written { pub fn header(h: &Header, mut w: impl Write) -> Written {
match h { match h {
Header::Special(h) => match h { Header::Special(h) => match h {