[uhttp] client
This commit is contained in:
parent
f548be3ea3
commit
9d6d8b672e
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
crates/uhttp/examples/client.rs
Normal file
71
crates/uhttp/examples/client.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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()))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue