Compare commits
2 commits
da94c17fe8
...
879003024c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
879003024c | ||
|
|
21e8b9c8bb |
51
Cargo.lock
generated
51
Cargo.lock
generated
|
|
@ -1,56 +1,7 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytes"
|
|
||||||
version = "1.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fetchplz"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"http",
|
|
||||||
"httplz",
|
|
||||||
"httplzx",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fnv"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "http"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"fnv",
|
|
||||||
"itoa",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httplz"
|
name = "httplz"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
|
||||||
"httplzx",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httplzx"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"http",
|
|
||||||
"httplz",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
|
||||||
|
|
|
||||||
10
Cargo.toml
10
Cargo.toml
|
|
@ -7,17 +7,17 @@ description = "A sans-io, no-std HTTP implementation"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/fetchplz",
|
# "crates/fetchplz",
|
||||||
"crates/httplzx"
|
# "crates/httplzx"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
httplz = { path = ".", features = ["std"] }
|
httplz = { path = ".", features = ["std"] }
|
||||||
httplzx = { path = "./crates/httplzx" }
|
# httplzx = { path = "./crates/httplzx" }
|
||||||
fetchplz = { path = "./crates/fetchplz" }
|
# fetchplz = { path = "./crates/fetchplz" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = []
|
std = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
httplzx = { path = "./crates/httplzx" }
|
# httplzx = { path = "./crates/httplzx" }
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,8 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
httplz = { workspace = true }
|
httplz = { workspace = true }
|
||||||
httplzx = { workspace = true }
|
httplzx = { workspace = true }
|
||||||
|
|
||||||
http = { version = "1.1.0" }
|
http = { version = "1.1.0" }
|
||||||
|
futures-lite = { version = "2.3.0" }
|
||||||
|
|
||||||
|
wove = { path = "../../../wove" }
|
||||||
|
|
|
||||||
|
|
@ -1,181 +1,119 @@
|
||||||
use std::io::{Error as IOError, Read, Write};
|
use std::future::Future;
|
||||||
use std::net::TcpStream;
|
use std::io::Error as IoError;
|
||||||
use std::str::FromStr;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use httplz::NeedsMoreData;
|
use futures_lite::{Stream, StreamExt};
|
||||||
use httplzx::ToEvents;
|
use http::uri::Scheme;
|
||||||
|
|
||||||
pub struct Body {
|
use http::{HeaderValue, Response};
|
||||||
buf: httplz::Buf<Box<[u8]>, u8>,
|
use wove::io::{AsyncReadLoan, AsyncWriteLoan, Transpose};
|
||||||
chunk_end: usize,
|
use wove::io_impl::IoImpl;
|
||||||
read_amt: usize,
|
use wove::net::TcpStream;
|
||||||
stream: TcpStream,
|
use wove::streams::StreamExt as _;
|
||||||
conn: httplz::Connection,
|
use wove::util::Ignore;
|
||||||
}
|
|
||||||
impl Body {
|
|
||||||
fn find_next_chunk_sz(&mut self) -> Result<(), IOError> {
|
|
||||||
self.buf.pop_front(self.chunk_end);
|
|
||||||
self.chunk_end = 0;
|
|
||||||
self.read_amt = 0;
|
|
||||||
|
|
||||||
while self.conn.is_recving() {
|
|
||||||
dbg!(self.conn, String::from_utf8_lossy(self.buf.filled()));
|
|
||||||
let res = self.conn.poll_recv(self.buf.filled());
|
|
||||||
if res.needs_more_data() {
|
|
||||||
self.buf.read_from(&mut self.stream)?;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (amt, ev) = res.map_err(|e| IOError::other(format!("{e:?}")))?;
|
|
||||||
match ev {
|
|
||||||
httplz::Event::BodyChunk(b) => {
|
|
||||||
self.chunk_end = b.len();
|
|
||||||
return Ok(());
|
|
||||||
},
|
|
||||||
_ => self.buf.pop_front(amt),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Read for Body {
|
|
||||||
fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result<usize> {
|
|
||||||
if self.chunk_end == self.read_amt {
|
|
||||||
self.find_next_chunk_sz()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let chunk = &self.buf.filled()[self.read_amt..self.chunk_end];
|
|
||||||
let amt = buf.write(chunk)?;
|
|
||||||
self.read_amt += amt;
|
|
||||||
|
|
||||||
Ok(amt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Body {
|
|
||||||
pub fn collect_bytes(self) -> Result<Vec<u8>, IOError> {
|
|
||||||
self.bytes().collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Response = http::Response<Body>;
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
IOError(IOError),
|
InvalidUriScheme,
|
||||||
HttplzError(httplz::Error),
|
|
||||||
InvalidUri,
|
|
||||||
InvalidStatusCode,
|
InvalidStatusCode,
|
||||||
InvalidHeaderValue,
|
InvalidHeaderValue,
|
||||||
|
InvalidHeaderName,
|
||||||
|
IoError(IoError),
|
||||||
|
Httplz(httplz::Error),
|
||||||
}
|
}
|
||||||
impl From<IOError> for Error {
|
impl From<IoError> for Error {
|
||||||
fn from(value: IOError) -> Self {
|
fn from(value: IoError) -> Self {
|
||||||
Self::IOError(value)
|
Self::IoError(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<httplz::Error> for Error {
|
impl From<httplz::Error> for Error {
|
||||||
fn from(value: httplz::Error) -> Self {
|
fn from(value: httplz::Error) -> Self {
|
||||||
Self::HttplzError(value)
|
Self::Httplz(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(path: &str) -> Result<Response, Error> {
|
impl From<http::status::InvalidStatusCode> for Error {
|
||||||
let uri = http::Uri::from_str(path).map_err(|_| Error::InvalidUri)?;
|
fn from(_: http::status::InvalidStatusCode) -> Self {
|
||||||
let scheme = uri.scheme().cloned().unwrap_or(http::uri::Scheme::HTTPS);
|
Self::InvalidStatusCode
|
||||||
let host = uri.host().ok_or(Error::InvalidUri)?;
|
|
||||||
let mut stream = TcpStream::connect(format!(
|
|
||||||
"{}:{}",
|
|
||||||
host,
|
|
||||||
uri.port_u16().unwrap_or_else(|| match scheme.as_str() {
|
|
||||||
"http" => 80,
|
|
||||||
_ => 443,
|
|
||||||
})
|
|
||||||
))?;
|
|
||||||
let mut conn = httplz::Connection::new(httplz::Role::Client);
|
|
||||||
|
|
||||||
let (mut parts, _) = http::Request::new(()).into_parts();
|
|
||||||
parts
|
|
||||||
.headers
|
|
||||||
.insert("Host", http::HeaderValue::from_str(host).unwrap());
|
|
||||||
parts.uri = uri;
|
|
||||||
|
|
||||||
let mut events = parts.to_events();
|
|
||||||
events.extend_from_slice(&[httplz::Event::HeadersDone, httplz::Event::SendDone]);
|
|
||||||
|
|
||||||
dbg!("a");
|
|
||||||
let mut buf = httplz::Buf::new(vec![0; 4096].into_boxed_slice());
|
|
||||||
for event in events {
|
|
||||||
conn.handle_send(&event, &mut buf)?;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stream.write_all(buf.filled())?;
|
impl From<http::header::InvalidHeaderValue> for Error {
|
||||||
|
fn from(_: http::header::InvalidHeaderValue) -> Self {
|
||||||
|
Self::InvalidHeaderValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert!(conn.is_recving());
|
impl From<http::header::InvalidHeaderName> for Error {
|
||||||
|
fn from(_: http::header::InvalidHeaderName) -> Self {
|
||||||
|
Self::InvalidHeaderName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type FetchResult<T> = Result<T, Error>;
|
||||||
|
|
||||||
|
pub struct Body<S> {
|
||||||
|
pub size_hint: Option<usize>,
|
||||||
|
pub stream: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type FetchResponse<S> = http::Response<Body<S>>;
|
||||||
|
pub type FetchRequest<S> = http::Request<Body<S>>;
|
||||||
|
|
||||||
|
pub trait RequestExt<S, I> {
|
||||||
|
fn send(
|
||||||
|
self,
|
||||||
|
io: &I,
|
||||||
|
) -> impl Future<
|
||||||
|
Output = FetchResult<FetchResponse<impl Stream<Item = Result<Box<[u8]>, IoError>>>>,
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, I: IoImpl> RequestExt<S, I> for http::Request<Body<S>>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Box<[u8]>>,
|
||||||
|
I: IoImpl,
|
||||||
|
I::TcpStream: AsyncReadLoan + AsyncWriteLoan,
|
||||||
|
{
|
||||||
|
async fn send(
|
||||||
|
self,
|
||||||
|
io: &I,
|
||||||
|
) -> FetchResult<FetchResponse<impl Stream<Item = Result<Box<[u8]>, IoError>>>> {
|
||||||
|
let mut stream = TcpStream::connect(io, "google.com:80").await?;
|
||||||
|
stream.write("GET / HTTP/1.1\r\n\r\n").await.transpose()?;
|
||||||
|
|
||||||
|
let mut conn = httplz::Connection::new(httplz::Role::Client);
|
||||||
|
conn.start_recving();
|
||||||
|
|
||||||
buf.clear();
|
|
||||||
let (mut parts, _) = http::Response::new(()).into_parts();
|
let (mut parts, _) = http::Response::new(()).into_parts();
|
||||||
|
|
||||||
while conn.is_recving() {
|
let mut leftovers = Vec::new();
|
||||||
let data = buf.filled();
|
while let Some(data) = stream.read_stream().next2().await {
|
||||||
let res = conn.poll_recv(data);
|
let data = data?;
|
||||||
if res.needs_more_data() {
|
leftovers.extend_from_slice(data.as_slice());
|
||||||
buf.read_from(&mut stream)?;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (amt, event) = res?;
|
todo!()
|
||||||
let mut brk = false;
|
as FetchResult<
|
||||||
match event {
|
FetchResponse<Pin<Box<dyn Stream<Item = Result<Box<[u8]>, IoError>>>>>,
|
||||||
httplz::Event::StatusLine(sl) => {
|
>
|
||||||
parts.status = http::StatusCode::from_u16(sl.status_code)
|
|
||||||
.map_err(|_| Error::InvalidStatusCode)?;
|
|
||||||
parts.version = http::Version::HTTP_11;
|
|
||||||
},
|
|
||||||
httplz::Event::Header(h) => {
|
|
||||||
parts.headers.insert(
|
|
||||||
http::HeaderName::from_str(h.name).unwrap(),
|
|
||||||
http::HeaderValue::from_bytes(h.value)
|
|
||||||
.map_err(|_| Error::InvalidHeaderValue)?,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
httplz::Event::HeadersDone => brk = true,
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
dbg!(amt, brk);
|
|
||||||
buf.pop_front(amt);
|
|
||||||
|
|
||||||
if brk {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(http::Response::from_parts(
|
|
||||||
parts,
|
|
||||||
Body {
|
|
||||||
buf,
|
|
||||||
stream,
|
|
||||||
conn,
|
|
||||||
chunk_end: 0,
|
|
||||||
read_amt: 0,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_fetch {
|
mod test {
|
||||||
use crate::fetch;
|
use futures_lite::StreamExt;
|
||||||
|
use http::Request;
|
||||||
|
use wove::{
|
||||||
|
io::{AsyncReadLoan, AsyncWriteLoan, Transpose},
|
||||||
|
io_impl::io_uring::IoUring,
|
||||||
|
streams::StreamExt as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Body, RequestExt};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fetch_1() {
|
fn test() {
|
||||||
let resp = fetch("http://httpbin.org/get")
|
|
||||||
.unwrap()
|
|
||||||
.map(|b| b.collect_bytes().unwrap());
|
|
||||||
let body = resp.into_body();
|
|
||||||
|
|
||||||
let body = String::from_utf8_lossy(&body);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
body,
|
|
||||||
include_str!("../testing/snapshots/httpbin_get_empty.txt")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
pub fn main() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
//! A simple client that POSTs some data to a path
|
//! A simple client that POSTs some data to a path
|
||||||
//!
|
//!
|
||||||
//! Usage:
|
//! Usage:
|
||||||
|
|
@ -27,7 +32,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
httplz::HeaderOther::from(("Host", host.as_bytes())).into(),
|
httplz::HeaderOther::from(("Host", host.as_bytes())).into(),
|
||||||
httplz::HeaderOther::from(("Accept", "*/*")).into(),
|
httplz::HeaderOther::from(("Accept", "**")).into(),
|
||||||
httplz::HeaderSpecial::ContentLength(data.len()).into(),
|
httplz::HeaderSpecial::ContentLength(data.len()).into(),
|
||||||
httplz::Event::HeadersDone,
|
httplz::Event::HeadersDone,
|
||||||
httplz::Event::BodyChunk(data.as_bytes()),
|
httplz::Event::BodyChunk(data.as_bytes()),
|
||||||
|
|
@ -70,3 +75,4 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
pub fn main() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
//! A simple echo server that echoes the body of a POST request, and returns a
|
//! A simple echo server that echoes the body of a POST request, and returns a
|
||||||
//! 405 for any other method
|
//! 405 for any other method
|
||||||
//!
|
//!
|
||||||
|
|
@ -88,3 +93,4 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
10
flake.nix
10
flake.nix
|
|
@ -21,7 +21,8 @@
|
||||||
pkgs = i.nixpkgs.legacyPackages.${system};
|
pkgs = i.nixpkgs.legacyPackages.${system};
|
||||||
pkgsDeno = i.deno-flake.packages.${system};
|
pkgsDeno = i.deno-flake.packages.${system};
|
||||||
pkgsFenix = i.fenix.packages.${system};
|
pkgsFenix = i.fenix.packages.${system};
|
||||||
nightly = pkgsFenix.default;
|
minimal = pkgsFenix.minimal;
|
||||||
|
complete = pkgsFenix.complete;
|
||||||
stable = pkgsFenix.stable;
|
stable = pkgsFenix.stable;
|
||||||
in {
|
in {
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
|
|
@ -34,11 +35,10 @@
|
||||||
pkgs.fd
|
pkgs.fd
|
||||||
|
|
||||||
(pkgsFenix.combine [
|
(pkgsFenix.combine [
|
||||||
(stable.withComponents [
|
(complete.withComponents [ "rust-src" "rust-analyzer" "rustfmt" "clippy" ])
|
||||||
"cargo" "rustc" "rust-src" "rust-analyzer" "clippy"
|
(minimal.withComponents [ "cargo" "rustc" "rust-std" ])
|
||||||
])
|
|
||||||
nightly.rustfmt
|
|
||||||
])
|
])
|
||||||
|
|
||||||
pkgs.cargo-watch
|
pkgs.cargo-watch
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
102
src/http.rs
Normal file
102
src/http.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use crate::parse::{Line, Parse, ParseError};
|
||||||
|
use core::str::from_utf8;
|
||||||
|
|
||||||
|
pub const LINE_ENDING: &[u8] = b"\r\n";
|
||||||
|
|
||||||
|
macro_rules! versions {
|
||||||
|
($(($name:ident, $str:expr)),* $(,)?) => {
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub enum Version<'a> {
|
||||||
|
$($name,)*
|
||||||
|
Unknown(&'a [u8]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parse<'a> for Version<'a> {
|
||||||
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
$({
|
||||||
|
const STR: &[u8] = $str.as_bytes();
|
||||||
|
let len = ((STR.len() <= data.len()) as usize) * STR.len();
|
||||||
|
|
||||||
|
let data = &data[..len];
|
||||||
|
if data.eq_ignore_ascii_case(STR) {
|
||||||
|
return Ok((STR.len(), Self::$name));
|
||||||
|
}
|
||||||
|
};)*
|
||||||
|
|
||||||
|
Ok((0, Self::Unknown(data)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
versions! {
|
||||||
|
(V1_0, "http/1.0"),
|
||||||
|
(V1_1, "http/1.1"),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! methods {
|
||||||
|
($(($name:ident, $str:expr)),* $(,)?) => {
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub enum Method<'a> {
|
||||||
|
$($name,)*
|
||||||
|
Other(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<'a> Parse<'a> for Method<'a> {
|
||||||
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
$({
|
||||||
|
const STR: &[u8] = $str.as_bytes();
|
||||||
|
let len = ((STR.len() <= data.len()) as usize) * STR.len();
|
||||||
|
|
||||||
|
let data = &data[..len];
|
||||||
|
if data.eq_ignore_ascii_case(STR) {
|
||||||
|
return Ok((STR.len(), Self::$name));
|
||||||
|
}
|
||||||
|
};)*
|
||||||
|
|
||||||
|
from_utf8(data).map(|s| (s.len(), Self::Other(s))).map_err(|_| Some(ParseError {
|
||||||
|
ctx: "method",
|
||||||
|
details: "unknown method was not a valid str",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
methods! {
|
||||||
|
(Get, "GET"),
|
||||||
|
(Head, "HEAD"),
|
||||||
|
(Options, "OPTIONS"),
|
||||||
|
(Trace, "TRACE"),
|
||||||
|
(Put, "PUT"),
|
||||||
|
(Delete, "DELETE"),
|
||||||
|
(Post, "POST"),
|
||||||
|
(Patch, "PATCH"),
|
||||||
|
(Connect, "CONNECT"),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Status<'a> {
|
||||||
|
pub code: u16,
|
||||||
|
pub text: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct RequestLine<'a> {
|
||||||
|
pub method: Method<'a>,
|
||||||
|
pub target: &'a str,
|
||||||
|
pub version: Version<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct StatusLine<'a> {
|
||||||
|
pub version: Version<'a>,
|
||||||
|
pub status: Status<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Header<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub value: &'a [u8],
|
||||||
|
}
|
||||||
403
src/lib.rs
403
src/lib.rs
|
|
@ -1,297 +1,176 @@
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
pub use http::*;
|
||||||
|
use parse::{Parse, ParseError};
|
||||||
|
use util::ResultTupExt;
|
||||||
|
|
||||||
pub mod common;
|
pub mod http;
|
||||||
pub mod parse;
|
pub mod parse;
|
||||||
pub mod parts;
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod write;
|
|
||||||
|
|
||||||
pub use common::*;
|
#[cfg(test)]
|
||||||
pub use parse::{NeedsMoreData, Parse};
|
mod test;
|
||||||
pub use parts::*;
|
|
||||||
pub use util::*;
|
|
||||||
pub use write::{Write, WriteCursor, Written};
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
pub enum Event<'a> {
|
pub enum Error {
|
||||||
#[default]
|
ParseError(ParseError),
|
||||||
Empty,
|
|
||||||
RequestLine(RequestLine<'a>),
|
|
||||||
Header(Header<'a>),
|
|
||||||
HeadersDone,
|
|
||||||
BodyChunk(&'a [u8]),
|
|
||||||
RecvDone,
|
|
||||||
StatusLine(StatusLine<'a>),
|
|
||||||
SendDone,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<Header<'a>> for Event<'a> {
|
impl From<ParseError> for Error {
|
||||||
fn from(value: Header<'a>) -> Self {
|
fn from(v: ParseError) -> Self {
|
||||||
Self::Header(value)
|
Self::ParseError(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<RequestLine<'a>> for Event<'a> {
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
fn from(value: RequestLine<'a>) -> Self {
|
|
||||||
Self::RequestLine(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<StatusLine<'a>> for Event<'a> {
|
|
||||||
fn from(value: StatusLine<'a>) -> Self {
|
|
||||||
Self::StatusLine(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Header { name, value }) => write!(
|
|
||||||
f,
|
|
||||||
"{}: {}",
|
|
||||||
name,
|
|
||||||
core::str::from_utf8(value).unwrap_or("<binary>")
|
|
||||||
),
|
|
||||||
Event::BodyChunk(b) => {
|
|
||||||
write!(f, "{}", core::str::from_utf8(b).unwrap_or("<binary>"))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
Client,
|
Client,
|
||||||
Server,
|
Server,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
enum BodyState {
|
pub enum BodyState {
|
||||||
ContentLength(usize),
|
ContentLength(usize),
|
||||||
Chunked,
|
Chunked,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
enum StateRecv {
|
pub enum State {
|
||||||
StartLine,
|
StartLine,
|
||||||
Headers(Option<BodyState>),
|
Headers(Option<BodyState>),
|
||||||
Body(BodyState),
|
Body(Option<BodyState>),
|
||||||
ValidateDone,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
enum StateSend {
|
|
||||||
StartLine,
|
|
||||||
Headers(Option<BodyState>),
|
|
||||||
Body(BodyState),
|
|
||||||
ValidateDone,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
enum StateConnection {
|
|
||||||
Recv(StateRecv),
|
|
||||||
Send(StateSend),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct Connection {
|
pub struct Connection {
|
||||||
|
keep_alive: bool,
|
||||||
role: Role,
|
role: Role,
|
||||||
state: StateConnection,
|
send: State,
|
||||||
|
recv: State,
|
||||||
}
|
}
|
||||||
impl Connection {
|
impl Connection {
|
||||||
pub fn new(role: Role) -> Self {
|
pub fn role(&self) -> Role {
|
||||||
let state = match role {
|
self.role
|
||||||
Role::Server => StateConnection::Recv(StateRecv::StartLine),
|
|
||||||
Role::Client => StateConnection::Send(StateSend::StartLine),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self { state, role }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_sending(&self) -> bool {
|
pub fn send(&self) -> State {
|
||||||
matches!(self.state, StateConnection::Send(_))
|
self.send
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_recving(&self) -> bool {
|
pub fn recv(&self) -> State {
|
||||||
matches!(self.state, StateConnection::Recv(_))
|
self.recv
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll_recv<'a>(&mut self, bytes: &'a [u8]) -> Parse<'a, Event<'a>> {
|
pub fn keep_alive(&self) -> bool {
|
||||||
let recv = match &self.state {
|
self.keep_alive
|
||||||
StateConnection::Recv(r) => r,
|
}
|
||||||
_ => {
|
}
|
||||||
return Err(ErrorKind::InvalidConnectionState.into());
|
|
||||||
},
|
impl Connection {
|
||||||
};
|
pub fn new(role: Role) -> Self {
|
||||||
|
Self {
|
||||||
let n = match (self.role, recv) {
|
keep_alive: true,
|
||||||
(Role::Server, StateRecv::StartLine) => parse::request_line(bytes).map2(|rl| {
|
role,
|
||||||
(
|
send: State::StartLine,
|
||||||
Event::RequestLine(rl),
|
recv: State::StartLine,
|
||||||
StateConnection::Recv(StateRecv::Headers(None)),
|
}
|
||||||
)
|
}
|
||||||
}),
|
|
||||||
|
pub fn recv_iter<'a>(&'a mut self, data: &'a [u8]) -> RecvIter<'a> {
|
||||||
(Role::Client, StateRecv::StartLine) => parse::status_line(bytes).map2(|rl| {
|
RecvIter {
|
||||||
(
|
conn: self,
|
||||||
Event::StatusLine(rl),
|
data,
|
||||||
StateConnection::Recv(StateRecv::Headers(None)),
|
read_amt: 0,
|
||||||
)
|
err: None,
|
||||||
}),
|
}
|
||||||
|
}
|
||||||
(_, StateRecv::Headers(body_state)) => {
|
}
|
||||||
if bytes.starts_with(b"\r\n") {
|
|
||||||
Ok((
|
pub struct RecvIter<'a> {
|
||||||
2,
|
conn: &'a mut Connection,
|
||||||
(
|
data: &'a [u8],
|
||||||
Event::HeadersDone,
|
read_amt: usize,
|
||||||
match body_state {
|
err: Option<Error>,
|
||||||
None => StateConnection::Recv(StateRecv::ValidateDone),
|
}
|
||||||
Some(b) => StateConnection::Recv(StateRecv::Body(*b)),
|
|
||||||
},
|
impl<'a> Iterator for RecvIter<'a> {
|
||||||
),
|
type Item = Event<'a>;
|
||||||
))
|
|
||||||
} else {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
parse::header(bytes).map2(|h| {
|
if self.err.is_some() {
|
||||||
let b = h
|
return None;
|
||||||
.special()
|
}
|
||||||
.map(|h| match h {
|
|
||||||
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
let data = &self.data[self.read_amt..];
|
||||||
HeaderSpecial::ContentLength(c) => BodyState::ContentLength(c),
|
|
||||||
})
|
let (res, next) = match (self.conn.role, self.conn.recv) {
|
||||||
.or(*body_state);
|
(Role::Client, State::StartLine) => (
|
||||||
|
StatusLine::parse(data).map2(Event::from),
|
||||||
(
|
State::Headers(None),
|
||||||
Event::Header(h),
|
),
|
||||||
StateConnection::Recv(StateRecv::Headers(b)),
|
|
||||||
)
|
(Role::Server, State::StartLine) => (
|
||||||
})
|
RequestLine::parse(data).map2(Event::from),
|
||||||
}
|
State::Headers(None),
|
||||||
},
|
),
|
||||||
|
|
||||||
(_, StateRecv::Body(body_state)) => match body_state {
|
(_, State::Headers(body_state)) => {
|
||||||
BodyState::ContentLength(remaining) => {
|
if data.starts_with(LINE_ENDING) {
|
||||||
if bytes.is_empty() && *remaining != 0 {
|
(
|
||||||
return fail(ErrorKind::NeedMoreData);
|
Ok((LINE_ENDING.len(), Event::HeadersEnd)),
|
||||||
}
|
State::Body(body_state),
|
||||||
if bytes.len() < *remaining {
|
)
|
||||||
Ok((
|
} else {
|
||||||
bytes.len(),
|
(
|
||||||
(
|
Header::parse(data).map2(Event::from),
|
||||||
Event::BodyChunk(bytes),
|
State::Headers(body_state),
|
||||||
StateConnection::Recv(StateRecv::Body(
|
)
|
||||||
BodyState::ContentLength(remaining - bytes.len()),
|
}
|
||||||
)),
|
},
|
||||||
),
|
|
||||||
))
|
(_, State::Body(None)) => (Ok((0, Event::MessageEnd)), State::StartLine),
|
||||||
} else {
|
(_, State::Body(Some(body_state))) => todo!(),
|
||||||
Ok((
|
};
|
||||||
*remaining,
|
|
||||||
(
|
let (amt, event) = match res {
|
||||||
Event::BodyChunk(&bytes[..*remaining]),
|
Ok(v) => v,
|
||||||
StateConnection::Recv(StateRecv::ValidateDone),
|
Err(Some(e)) => {
|
||||||
),
|
self.err = Some(e.into());
|
||||||
))
|
return None;
|
||||||
}
|
},
|
||||||
},
|
Err(None) => {
|
||||||
_ => todo!(),
|
return None;
|
||||||
},
|
},
|
||||||
|
};
|
||||||
(_, StateRecv::ValidateDone) => {
|
|
||||||
if bytes.is_empty() {
|
self.read_amt += amt;
|
||||||
Ok((
|
self.conn.recv = next;
|
||||||
0,
|
|
||||||
(Event::RecvDone, StateConnection::Send(StateSend::StartLine)),
|
Some(event)
|
||||||
))
|
}
|
||||||
} else {
|
}
|
||||||
fail(ErrorKind::TrailingBytes)
|
|
||||||
}
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
},
|
pub enum Event<'a> {
|
||||||
};
|
RequestLine(RequestLine<'a>),
|
||||||
|
StatusLine(StatusLine<'a>),
|
||||||
n.map2(move |(ev, next_state)| {
|
Header(Header<'a>),
|
||||||
self.state = next_state;
|
HeadersEnd,
|
||||||
|
MessageEnd,
|
||||||
ev
|
}
|
||||||
})
|
|
||||||
}
|
impl<'a> From<Header<'a>> for Event<'a> {
|
||||||
|
fn from(v: Header<'a>) -> Self {
|
||||||
pub fn handle_send(&mut self, event: &Event, mut w: impl Write) -> Written {
|
Self::Header(v)
|
||||||
let state = match self.state {
|
}
|
||||||
StateConnection::Send(s) => s,
|
}
|
||||||
_ => return fail(ErrorKind::InvalidConnectionState),
|
|
||||||
};
|
impl<'a> From<StatusLine<'a>> for Event<'a> {
|
||||||
|
fn from(v: StatusLine<'a>) -> Self {
|
||||||
let next = match (self.role, state, event) {
|
Self::StatusLine(v)
|
||||||
(Role::Server, StateSend::StartLine, Event::StatusLine(sl)) => {
|
}
|
||||||
write::status_line(sl, w)
|
}
|
||||||
.map(|_| StateConnection::Send(StateSend::Headers(None)))
|
|
||||||
},
|
impl<'a> From<RequestLine<'a>> for Event<'a> {
|
||||||
|
fn from(v: RequestLine<'a>) -> Self {
|
||||||
(Role::Client, StateSend::StartLine, Event::RequestLine(rl)) => {
|
Self::RequestLine(v)
|
||||||
write::request_line(rl, w)
|
|
||||||
.map(|_| StateConnection::Send(StateSend::Headers(None)))
|
|
||||||
},
|
|
||||||
|
|
||||||
(_, StateSend::Headers(body_state), Event::Header(h)) => {
|
|
||||||
write::header(h, w)?;
|
|
||||||
|
|
||||||
let bs = h
|
|
||||||
.special()
|
|
||||||
.map(|h| match h {
|
|
||||||
HeaderSpecial::TransferEncodingChunked => BodyState::Chunked,
|
|
||||||
HeaderSpecial::ContentLength(cl) => BodyState::ContentLength(cl),
|
|
||||||
})
|
|
||||||
.or(body_state);
|
|
||||||
|
|
||||||
Ok(StateConnection::Send(StateSend::Headers(bs)))
|
|
||||||
},
|
|
||||||
|
|
||||||
(_, StateSend::Headers(body_state), Event::HeadersDone) => {
|
|
||||||
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::StartLine))
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => return Err(Error::from(ErrorKind::InvalidEventForConnectionState)),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
self.state = next;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
212
src/parse.rs
212
src/parse.rs
|
|
@ -1,142 +1,144 @@
|
||||||
|
use std::str::from_utf8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fail_details,
|
http::{Header, Method, RequestLine, Status, StatusLine, Version},
|
||||||
parts::{RequestLine, Version},
|
LINE_ENDING,
|
||||||
Error, ErrorKind, Header, Method, StatusLine,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Parse<'a, T> = Result<(usize, T), Error>;
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
pub trait NeedsMoreData {
|
pub struct ParseError {
|
||||||
fn needs_more_data(&self) -> bool;
|
pub ctx: &'static str,
|
||||||
|
pub details: &'static str,
|
||||||
}
|
}
|
||||||
impl<'a, T> NeedsMoreData for Parse<'a, T> {
|
|
||||||
fn needs_more_data(&self) -> bool {
|
pub trait Parse<'a>: Sized {
|
||||||
self.as_ref()
|
/// `None` in the return indicates that the parser is not applicable
|
||||||
.err()
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>>;
|
||||||
.map(|e| e.kind)
|
}
|
||||||
.is_some_and(|e| e == ErrorKind::NeedMoreData)
|
|
||||||
|
pub struct Line<'a>(pub &'a [u8]);
|
||||||
|
impl<'a> Parse<'a> for Line<'a> {
|
||||||
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
let rn_idx = data.windows(2).position(|w| w == LINE_ENDING).ok_or(None)?;
|
||||||
|
|
||||||
|
Ok((rn_idx + 2, Line(&data[..rn_idx])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split_crlf(d: &[u8]) -> Option<(&[u8], usize)> {
|
impl<'a> Parse<'a> for Status<'a> {
|
||||||
let p = d.windows(2).position(|w| w == b"\r\n")?;
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
let mut skipped_chars = 0;
|
||||||
|
let mut parts = data
|
||||||
|
.split(|c| {
|
||||||
|
let b = c.is_ascii_whitespace();
|
||||||
|
skipped_chars += b as usize;
|
||||||
|
b
|
||||||
|
})
|
||||||
|
.filter(|b| !b.is_empty());
|
||||||
|
|
||||||
Some((&d[..p], p + 2))
|
let code = parts.next().ok_or(ParseError {
|
||||||
|
ctx: "status",
|
||||||
|
details: "missing status code",
|
||||||
|
})?;
|
||||||
|
let code_len = code.len();
|
||||||
|
let text = parts.next().unwrap_or(b"");
|
||||||
|
let text_len = text.len();
|
||||||
|
|
||||||
|
let code = from_utf8(code).map_err(|_| ParseError {
|
||||||
|
ctx: "status code",
|
||||||
|
details: "invalid utf8",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let code: u16 = code.parse().map_err(|_| ParseError {
|
||||||
|
ctx: "status code",
|
||||||
|
details: "not a number",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let text = from_utf8(text).map_err(|_| ParseError {
|
||||||
|
ctx: "status text",
|
||||||
|
details: "invalid utf8",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((code_len + text_len + skipped_chars, Status { code, text }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_line(d: &[u8]) -> Parse<RequestLine> {
|
impl<'a> Parse<'a> for RequestLine<'a> {
|
||||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
let (line_amt, Line(line)) = Line::parse(data)?;
|
||||||
let mut it = line
|
let mut tokens = line
|
||||||
.split(|b| b.is_ascii_whitespace())
|
.split(|b| b.is_ascii_whitespace())
|
||||||
.filter(|bs| !bs.is_empty());
|
.filter(|bs| !bs.is_empty());
|
||||||
|
|
||||||
let (method, target, version) = match (it.next(), it.next(), it.next()) {
|
let method = tokens.next().ok_or(ParseError {
|
||||||
(Some(m), Some(t), Some(v)) => (m, t, v),
|
ctx: "request line",
|
||||||
_ => {
|
details: "missing method",
|
||||||
return fail_details(
|
})?;
|
||||||
ErrorKind::Parse,
|
|
||||||
"request line doesn't have required number of elements",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let method = match core::str::from_utf8(method) {
|
let target = tokens.next().ok_or(ParseError {
|
||||||
Ok(m) => m,
|
ctx: "request line",
|
||||||
_ => {
|
details: "missing target",
|
||||||
return fail_details(ErrorKind::Parse, "expected method to be ascii");
|
})?;
|
||||||
},
|
|
||||||
};
|
|
||||||
let method = Method::new_from_str(method);
|
|
||||||
|
|
||||||
let target = match core::str::from_utf8(target) {
|
let version = tokens.next().ok_or(ParseError {
|
||||||
Ok(m) => m,
|
ctx: "request line",
|
||||||
_ => {
|
details: "missing version",
|
||||||
return fail_details(ErrorKind::Parse, "expected target to be ascii");
|
})?;
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let version = match () {
|
let (_, method) = Method::parse(method)?;
|
||||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
let target = from_utf8(target).map_err(|_| ParseError {
|
||||||
_ => {
|
ctx: "request line",
|
||||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
details: "target is not valid utf8",
|
||||||
},
|
})?;
|
||||||
};
|
let (_, version) = Version::parse(version)?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
amt,
|
line_amt,
|
||||||
RequestLine {
|
RequestLine {
|
||||||
method,
|
method,
|
||||||
target,
|
target,
|
||||||
version,
|
version,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_line(d: &[u8]) -> Parse<StatusLine> {
|
impl<'a> Parse<'a> for StatusLine<'a> {
|
||||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
let (line_amt, Line(line)) = Line::parse(data)?;
|
||||||
|
|
||||||
let mut it = line
|
let (version, status) =
|
||||||
.split(|b| b.is_ascii_whitespace())
|
line.split_at(line.iter().position(|b| b.is_ascii_whitespace()).ok_or(
|
||||||
.filter(|bs| !bs.is_empty());
|
ParseError {
|
||||||
|
ctx: "status line",
|
||||||
let (version, status_code, status_text) = match (it.next(), it.next(), it.next()) {
|
details: "missing status",
|
||||||
(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 () {
|
let (_, version) = Version::parse(version)?;
|
||||||
_ if version.eq_ignore_ascii_case(b"http/1.1") => Version::HTTP1_1,
|
let (_, status) = Status::parse(status.trim_ascii())?;
|
||||||
_ => {
|
|
||||||
return fail_details(ErrorKind::Parse, "unknown http version");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_code = core::str::from_utf8(status_code)
|
Ok((line_amt, Self { version, status }))
|
||||||
.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((
|
|
||||||
amt,
|
|
||||||
StatusLine {
|
|
||||||
version,
|
|
||||||
status_code,
|
|
||||||
status_text,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(d: &[u8]) -> Parse<Header> {
|
impl<'a> Parse<'a> for Header<'a> {
|
||||||
let (line, amt) = split_crlf(d).ok_or(Error::from(ErrorKind::NeedMoreData))?;
|
fn parse(data: &'a [u8]) -> Result<(usize, Self), Option<ParseError>> {
|
||||||
|
let (line_amt, Line(line)) = Line::parse(data)?;
|
||||||
|
|
||||||
let mut it = line.split(|b| *b == b':').filter(|bs| !bs.is_empty());
|
let (name, value) =
|
||||||
|
line.split_at(line.iter().position(|b| *b == b':').ok_or(ParseError {
|
||||||
|
ctx: "header",
|
||||||
|
details: "missing :",
|
||||||
|
})?);
|
||||||
|
|
||||||
let (name, value) = match (it.next(), it.next()) {
|
let name = from_utf8(name).map_err(|_| ParseError {
|
||||||
(Some(n), Some(v)) => (n, v),
|
ctx: "header",
|
||||||
_ => {
|
details: "name is not valid utf8",
|
||||||
return fail_details(
|
})?;
|
||||||
ErrorKind::Parse,
|
|
||||||
"header doesn't have required number of elements",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = match core::str::from_utf8(name) {
|
let value = &value[1..].trim_ascii();
|
||||||
Ok(m) => m,
|
|
||||||
_ => return fail_details(ErrorKind::Parse, "expected target to be ascii"),
|
|
||||||
};
|
|
||||||
let name = name.trim();
|
|
||||||
|
|
||||||
let value = value.trim_ascii();
|
Ok((line_amt, Self { name, value }))
|
||||||
|
}
|
||||||
Ok((amt, Header::from((name, value))))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/parts.rs
13
src/parts.rs
|
|
@ -40,7 +40,7 @@ impl<'a> Method<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> core::fmt::Display for Method<'a> {
|
impl core::fmt::Display for Method<'_> {
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
use Method::*;
|
use Method::*;
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ pub struct Header<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub value: &'a [u8],
|
pub value: &'a [u8],
|
||||||
}
|
}
|
||||||
impl<'a> Header<'a> {
|
impl Header<'_> {
|
||||||
pub fn special(&self) -> Option<HeaderSpecial> {
|
pub fn special(&self) -> Option<HeaderSpecial> {
|
||||||
let Self { name, value } = self;
|
let Self { name, value } = self;
|
||||||
|
|
||||||
|
|
@ -90,15 +90,10 @@ impl<'a> Header<'a> {
|
||||||
{
|
{
|
||||||
Some(HeaderSpecial::TransferEncodingChunked)
|
Some(HeaderSpecial::TransferEncodingChunked)
|
||||||
},
|
},
|
||||||
_ if name.eq_ignore_ascii_case("content-length") => {
|
_ if name.eq_ignore_ascii_case("content-length") => core::str::from_utf8(value)
|
||||||
match core::str::from_utf8(value)
|
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
{
|
.map(HeaderSpecial::ContentLength),
|
||||||
Some(v) => Some(HeaderSpecial::ContentLength(v)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
src/state.rs
Normal file
1
src/state.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
1
src/test/mod.rs
Normal file
1
src/test/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mod recv;
|
||||||
162
src/test/recv.rs
Normal file
162
src/test/recv.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
macro_rules! go {
|
||||||
|
($role:expr, $input:expr, $expected_events:expr) => {
|
||||||
|
let mut conn = Connection::new($role);
|
||||||
|
let data = $input;
|
||||||
|
let iter = conn.recv_iter(data.as_bytes());
|
||||||
|
let events: Vec<_> = iter.collect();
|
||||||
|
|
||||||
|
assert_eq!(events, $expected_events);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mod server {
|
||||||
|
use crate::{Connection, Event, Header, Method, RequestLine, Role, Version};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple() {
|
||||||
|
go!(
|
||||||
|
Role::Server,
|
||||||
|
"GET / HTTP/1.1\r\n\r\n",
|
||||||
|
[
|
||||||
|
RequestLine {
|
||||||
|
method: Method::Get,
|
||||||
|
target: "/",
|
||||||
|
version: Version::V1_1
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Event::HeadersEnd,
|
||||||
|
Event::MessageEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_one_header() {
|
||||||
|
go!(
|
||||||
|
Role::Server,
|
||||||
|
"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
||||||
|
[
|
||||||
|
RequestLine {
|
||||||
|
method: Method::Get,
|
||||||
|
target: "/",
|
||||||
|
version: Version::V1_1
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Header {
|
||||||
|
name: "Host",
|
||||||
|
value: b"localhost"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Event::HeadersEnd,
|
||||||
|
Event::MessageEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_two_headers() {
|
||||||
|
go!(
|
||||||
|
Role::Server,
|
||||||
|
"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n\r\n",
|
||||||
|
[
|
||||||
|
RequestLine {
|
||||||
|
method: Method::Get,
|
||||||
|
target: "/",
|
||||||
|
version: Version::V1_1
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Header {
|
||||||
|
name: "Host",
|
||||||
|
value: b"localhost"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Header {
|
||||||
|
name: "Accept",
|
||||||
|
value: b"*/*"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Event::HeadersEnd,
|
||||||
|
Event::MessageEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod client {
|
||||||
|
use crate::{Connection, Event, Header, Method, Role, Status, StatusLine, Version};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple() {
|
||||||
|
go!(
|
||||||
|
Role::Client,
|
||||||
|
"HTTP/1.1 200 OK\r\n\r\n",
|
||||||
|
[
|
||||||
|
StatusLine {
|
||||||
|
version: Version::V1_1,
|
||||||
|
status: Status {
|
||||||
|
code: 200,
|
||||||
|
text: "OK"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Event::HeadersEnd,
|
||||||
|
Event::MessageEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_one_header() {
|
||||||
|
go!(
|
||||||
|
Role::Client,
|
||||||
|
"HTTP/1.1 200 OK\r\nHost: localhost\r\n\r\n",
|
||||||
|
[
|
||||||
|
StatusLine {
|
||||||
|
version: Version::V1_1,
|
||||||
|
status: Status {
|
||||||
|
code: 200,
|
||||||
|
text: "OK"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Header {
|
||||||
|
name: "Host",
|
||||||
|
value: b"localhost"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Event::HeadersEnd,
|
||||||
|
Event::MessageEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_two_headers() {
|
||||||
|
go!(
|
||||||
|
Role::Client,
|
||||||
|
"HTTP/1.1 200 OK\r\nHost: localhost\r\nAccept: */*\r\n\r\n",
|
||||||
|
[
|
||||||
|
StatusLine {
|
||||||
|
version: Version::V1_1,
|
||||||
|
status: Status {
|
||||||
|
code: 200,
|
||||||
|
text: "OK"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Header {
|
||||||
|
name: "Host",
|
||||||
|
value: b"localhost"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Header {
|
||||||
|
name: "Accept",
|
||||||
|
value: b"*/*"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Event::HeadersEnd,
|
||||||
|
Event::MessageEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use crate::{ErrorKind, Write, Written};
|
|
||||||
|
|
||||||
pub trait ResultTupExt<A, T, E> {
|
pub trait ResultTupExt<A, T, E> {
|
||||||
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), E>;
|
fn map2<U>(self, f: impl FnOnce(T) -> U) -> Result<(A, U), E>;
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +10,7 @@ impl<A, T, E> ResultTupExt<A, T, E> for Result<(A, T), E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
pub struct NotEnoughSpace;
|
pub struct NotEnoughSpace;
|
||||||
pub struct Buf<T, U> {
|
pub struct Buf<T, U> {
|
||||||
written_len: usize,
|
written_len: usize,
|
||||||
|
|
@ -141,3 +140,4 @@ where
|
||||||
.map_err(|_| ErrorKind::BufNotBigEnough.into())
|
.map_err(|_| ErrorKind::BufNotBigEnough.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue