summaryrefslogtreecommitdiff
path: root/src/request.rs
diff options
context:
space:
mode:
authorJohn Turner <jturner.usa@gmail.com>2026-03-04 20:53:24 -0500
committerJohn Turner <jturner.usa@gmail.com>2026-03-04 20:53:32 -0500
commit4a16841789604614bc495c36972236749e5f35b0 (patch)
treeff7383c17f5d265967a1db083884fec78062e53f /src/request.rs
parent3c4208abd325d317c7524ba0dc3b701edfa9ebf8 (diff)
downloadhttpd-4a16841789604614bc495c36972236749e5f35b0.tar.gz
roll our own http types
Diffstat (limited to 'src/request.rs')
-rw-r--r--src/request.rs178
1 files changed, 152 insertions, 26 deletions
diff --git a/src/request.rs b/src/request.rs
index 2814a14..6da46ee 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -1,55 +1,181 @@
-
use mlua::UserData;
+use mon::{
+ Parser, ParserIter, any, ascii_alphanumeric, ascii_whitespace, input::InputIter, one_of, tag,
+ whitespace,
+};
+
+use strum::Display;
+use tokio::io::{AsyncBufRead, AsyncBufReadExt};
+
+use std::{collections::HashMap, io, num::ParseIntError, str};
-use tokio::io::{self};
+use get::Get;
#[derive(Debug, thiserror::Error)]
pub enum Error {
- #[error("error parsing request: {0}")]
- Parse(#[from] httparse::Error),
-
#[error("io error: {0}")]
Io(#[from] io::Error),
- #[error("invalid method: {0}")]
- Method(#[from] http::method::InvalidMethod),
+ #[error("parser error")]
+ Parse(usize),
- #[error("invalid request: {0}")]
- Request(#[from] http::Error),
+ #[error("parse int error: {0}")]
+ ParseInt(ParseIntError),
- #[error("unsupported version")]
- Version,
+ #[error("unicode error: {0}")]
+ Unicode(#[from] str::Utf8Error),
}
-#[derive(Debug, Clone)]
-pub struct Request<T>(http::Request<T>);
+#[derive(Debug, Clone, Copy, Display)]
+pub enum Method {
+ #[strum(to_string = "GET")]
+ Get,
-impl<T> Request<T> {
- pub fn inner(&self) -> &http::Request<T> {
- &self.0
- }
+ #[strum(to_string = "HEAD")]
+ Head,
+}
+
+#[derive(Debug, Clone, Get)]
+pub struct Path(#[get(method = "inner")] Vec<u8>);
+
+#[derive(Debug, Clone, Get)]
+pub struct Request {
+ method: Method,
+ path: Path,
+ headers: HashMap<String, Vec<u8>>,
+}
+
+impl Request {
+ pub async fn parse<T: AsyncBufRead + Unpin>(
+ mut reader: T,
+ buf: &mut Vec<u8>,
+ line: &mut Vec<u8>,
+ ) -> Result<Option<Self>, Error> {
+ buf.clear();
+
+ loop {
+ line.clear();
+
+ if reader.read_until(b'\n', line).await? == 0 {
+ return Ok(None);
+ }
+
+ if line == b"\r\n" {
+ break;
+ }
+
+ buf.extend_from_slice(line);
+ }
+
+ let (method, path, headers) = match parse().parse_finished(InputIter::new(buf)) {
+ Ok(((method, path), headers)) => (method, Path(path), headers),
+ Err(mon::ParserFinishedError::Err(e) | mon::ParserFinishedError::Unfinished(e)) => {
+ return Err(Error::Parse(e.position()));
+ }
+ };
+
+ let headers = headers
+ .into_iter()
+ .map(|(key, value)| {
+ Ok::<(String, Vec<u8>), str::Utf8Error>((
+ str::from_utf8(&key)?.to_lowercase(),
+ value,
+ ))
+ })
+ .collect::<Result<HashMap<String, Vec<u8>>, _>>()?;
- pub fn new(request: http::Request<T>) -> Self {
- Self(request)
+ Ok(Some(Self {
+ method,
+ path,
+ headers,
+ }))
}
}
-impl<T> UserData for Request<T> {
+fn method<'a>() -> impl Parser<&'a [u8], Output = Method> {
+ tag(b"GET".as_slice())
+ .map(|_| Method::Get)
+ .or(tag(b"HEAD".as_slice()).map(|_| Method::Head))
+}
+
+fn path<'a>() -> impl Parser<&'a [u8], Output = Vec<u8>> {
+ any().and_not(whitespace()).repeated().at_least(1)
+}
+
+fn header<'a>() -> impl Parser<&'a [u8], Output = (Vec<u8>, Vec<u8>)> {
+ let key = ascii_alphanumeric()
+ .followed_by(
+ ascii_alphanumeric()
+ .or(one_of(b"-".iter().copied()))
+ .repeated()
+ .many(),
+ )
+ .recognize()
+ .map(|output: &[u8]| output.to_vec());
+
+ let value = any()
+ .and_not(tag(b"\r\n".as_slice()))
+ .repeated()
+ .at_least(1);
+
+ key.and(value.preceded_by(tag(b": ".as_slice())))
+}
+
+impl UserData for Request {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
- fields.add_field_method_get("method", |_, this| {
- Ok(this.inner().method().as_str().to_string())
- });
+ fields.add_field_method_get("method", |_, this| Ok(this.method().to_string()));
- fields.add_field_method_get("path", |_, this| Ok(this.inner().uri().path().to_string()));
+ fields.add_field_method_get("path", |_, this| Ok(this.path().0.clone()));
fields.add_field_method_get("headers", |lua, this| {
let table = lua.create_table()?;
- for (key, value) in this.inner().headers() {
- table.set(key.as_str(), value.as_bytes())?;
+ for (key, value) in this.headers() {
+ table.set(key.clone(), value.clone())?;
}
Ok(table)
})
}
}
+
+#[allow(clippy::type_complexity)]
+fn parse<'a>() -> impl Parser<&'a [u8], Output = ((Method, Vec<u8>), Vec<(Vec<u8>, Vec<u8>)>)> {
+ method()
+ .followed_by(ascii_whitespace())
+ .and(path())
+ .followed_by(ascii_whitespace())
+ .followed_by(tag(b"HTTP/1.1\r\n".as_slice()))
+ .and(
+ header()
+ .separated_by_with_trailing(tag(b"\r\n".as_slice()))
+ .many(),
+ )
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_parse_header() {
+ let input = b"Content-Length: 100";
+
+ header().parse_finished(InputIter::new(input)).unwrap();
+ }
+
+ #[tokio::test]
+ async fn test_parse_get() {
+ let mut buf = Vec::new();
+ let mut line = Vec::new();
+ let input = b"GET /path HTTP/1.1\r\nContent-Length: 100\r\n";
+
+ match Request::parse(input.as_slice(), &mut buf, &mut line).await {
+ Ok(_) => (),
+ Err(Error::Parse(position)) => {
+ panic!("{}", &str::from_utf8(input).unwrap()[position..])
+ }
+ Err(e) => panic!("{e}"),
+ }
+ }
+}