From 9d6d8b672e6e1466f37e0a25ccc500849a4fd56a Mon Sep 17 00:00:00 2001 From: soup Date: Mon, 14 Oct 2024 22:17:19 -0400 Subject: [PATCH] [uhttp] client --- crates/uhttp/crates/uhttp-ext/src/lib.rs | 19 ++++- crates/uhttp/examples/client.rs | 71 ++++++++++++++++++ crates/uhttp/examples/echo.rs | 18 +++-- crates/uhttp/src/lib.rs | 91 +++++++++++++++++------- crates/uhttp/src/parse.rs | 46 +++++++++++- crates/uhttp/src/util.rs | 2 - crates/uhttp/src/write.rs | 6 +- 7 files changed, 219 insertions(+), 34 deletions(-) create mode 100644 crates/uhttp/examples/client.rs diff --git a/crates/uhttp/crates/uhttp-ext/src/lib.rs b/crates/uhttp/crates/uhttp-ext/src/lib.rs index c135c02..da573bf 100644 --- a/crates/uhttp/crates/uhttp-ext/src/lib.rs +++ b/crates/uhttp/crates/uhttp-ext/src/lib.rs @@ -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; } } diff --git a/crates/uhttp/examples/client.rs b/crates/uhttp/examples/client.rs new file mode 100644 index 0000000..ac92477 --- /dev/null +++ b/crates/uhttp/examples/client.rs @@ -0,0 +1,71 @@ +//! A simple client that POSTs some data to a path +//! +//! Usage: +//! cargo run --example client -- + +use std::{error::Error, io::Write, net::TcpStream}; + +use uhttp::Lift; + +pub fn main() -> Result<(), Box> { + 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::::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(()) +} diff --git a/crates/uhttp/examples/echo.rs b/crates/uhttp/examples/echo.rs index f480b64..9f36514 100644 --- a/crates/uhttp/examples/echo.rs +++ b/crates/uhttp/examples/echo.rs @@ -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 -- use std::{error::Error, io::Write, net::TcpListener}; use uhttp::Lift; fn main() -> Result<(), Box> { - 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::::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> { 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> { 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, ] diff --git a/crates/uhttp/src/lib.rs b/crates/uhttp/src/lib.rs index 5f06cf6..cde6dca 100644 --- a/crates/uhttp/src/lib.rs +++ b/crates/uhttp/src/lib.rs @@ -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("") + ) + }, + 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("")) + }, + } + } +} -#[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), Body(BodyState), ValidateDone, @@ -46,7 +76,7 @@ enum StateRecv { #[derive(Debug, Copy, Clone)] enum StateSend { - StatusLine, + StartLine, Headers(Option), 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)), diff --git a/crates/uhttp/src/parse.rs b/crates/uhttp/src/parse.rs index 834b423..b0b3da9 100644 --- a/crates/uhttp/src/parse.rs +++ b/crates/uhttp/src/parse.rs @@ -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 { go().tup(rest) } +pub fn status_line(d: &[u8]) -> Parse { + 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
{ let (line, rest) = split_crlf(d).ok_or((d, ErrorKind::NeedMoreData.into()))?; diff --git a/crates/uhttp/src/util.rs b/crates/uhttp/src/util.rs index 31414a6..ebd7e5a 100644 --- a/crates/uhttp/src/util.rs +++ b/crates/uhttp/src/util.rs @@ -1,5 +1,3 @@ -use crate::{ErrorKind, Write}; - pub trait ResultTupExt { fn map2(self, f: impl FnOnce(T) -> U) -> Result<(A, U), (B, E)>; } diff --git a/crates/uhttp/src/write.rs b/crates/uhttp/src/write.rs index b972ec6..ac3d2d0 100644 --- a/crates/uhttp/src/write.rs +++ b/crates/uhttp/src/write.rs @@ -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 { 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 {