[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.written_len += s.len();
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)
where
U: Copy,
@ -69,7 +86,7 @@ where
// - 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;
}
}

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 uhttp::Lift;
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 {
let (mut stream, _) = listener.accept()?;
let mut conn = uhttp::Connection::new(uhttp::Role::Server);
@ -50,7 +60,7 @@ fn main() -> Result<(), Box<dyn Error>> {
status_code: 405,
status_text: "Method not allowed",
}),
uhttp::Event::HeadersEnd,
uhttp::Event::HeadersDone,
uhttp::Event::SendDone,
]
} else {
@ -63,7 +73,7 @@ fn main() -> Result<(), Box<dyn Error>> {
uhttp::Event::Header(uhttp::Header::Special(
uhttp::HeaderSpecial::ContentLength(body.len()),
)),
uhttp::Event::HeadersEnd,
uhttp::Event::HeadersDone,
uhttp::Event::BodyChunk(body.as_slice()),
uhttp::Event::SendDone,
]

View file

@ -17,14 +17,44 @@ pub enum Event<'a> {
Empty,
RequestLine(RequestLine<'a>),
Header(Header<'a>),
HeadersEnd,
HeadersDone,
BodyChunk(&'a [u8]),
RecvDone,
StatusLine(StatusLine<'a>),
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 {
Client,
Server,
@ -38,7 +68,7 @@ enum BodyState {
#[derive(Debug, Copy, Clone)]
enum StateRecv {
RequestLine,
StartLine,
Headers(Option<BodyState>),
Body(BodyState),
ValidateDone,
@ -46,7 +76,7 @@ enum StateRecv {
#[derive(Debug, Copy, Clone)]
enum StateSend {
StatusLine,
StartLine,
Headers(Option<BodyState>),
Body(BodyState),
ValidateDone,
@ -58,7 +88,7 @@ enum StateConnection {
Send(StateSend),
}
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
pub struct Connection {
role: Role,
state: StateConnection,
@ -66,8 +96,8 @@ pub struct Connection {
impl Connection {
pub fn new(role: Role) -> Self {
let state = match role {
Role::Server => StateConnection::Recv(StateRecv::RequestLine),
Role::Client => StateConnection::Send(StateSend::StatusLine),
Role::Server => StateConnection::Recv(StateRecv::StartLine),
Role::Client => StateConnection::Send(StateSend::StartLine),
};
Self { state, role }
@ -89,20 +119,27 @@ impl Connection {
},
};
let n = match recv {
StateRecv::RequestLine => parse::request_line(bytes).map2(|rl| {
let n = match (self.role, recv) {
(Role::Server, StateRecv::StartLine) => parse::request_line(bytes).map2(|rl| {
(
Event::RequestLine(rl),
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") {
Ok((
&bytes[2..],
(
Event::HeadersEnd,
Event::HeadersDone,
match body_state {
None => StateConnection::Recv(StateRecv::ValidateDone),
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) => {
if bytes.len() < *remaining {
Ok((
@ -154,14 +191,11 @@ impl Connection {
_ => todo!(),
},
StateRecv::ValidateDone => {
(_, StateRecv::ValidateDone) => {
if bytes.is_empty() {
Ok((
bytes,
(
Event::RecvDone,
StateConnection::Send(StateSend::StatusLine),
),
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
))
} else {
fail(ErrorKind::TrailingBytes).tup(bytes)
@ -182,11 +216,18 @@ impl Connection {
_ => 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))),
let next = match (self.role, state, event) {
(Role::Server, StateSend::StartLine, Event::StatusLine(sl)) => {
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)?;
let bs = match h {
@ -202,7 +243,7 @@ impl Connection {
Ok(StateConnection::Send(StateSend::Headers(bs)))
},
(StateSend::Headers(body_state), Event::HeadersEnd) => {
(_, StateSend::Headers(body_state), Event::HeadersDone) => {
write!(w, "\r\n")?;
match body_state {
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 () {
_ if c.len() < cl => {
w.write(c)?;
@ -233,8 +274,8 @@ impl Connection {
BodyState::Chunked => todo!(),
},
(StateSend::ValidateDone, Event::SendDone) => {
Ok(StateConnection::Recv(StateRecv::RequestLine))
(_, StateSend::ValidateDone, Event::SendDone) => {
Ok(StateConnection::Recv(StateRecv::StartLine))
},
_ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)),

View file

@ -1,7 +1,7 @@
use crate::{
fail_details,
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)>;
@ -61,6 +61,50 @@ pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
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> {
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> {
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 crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, StatusLine};
use crate::{Error, ErrorKind, Header, HeaderOther, HeaderSpecial, RequestLine, StatusLine};
pub struct FmtWriteAdapter<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 {
match h {
Header::Special(h) => match h {