Files
urus/src/parser.rs
T
2026-05-26 23:16:45 +02:00

380 lines
13 KiB
Rust

//! HTTP/1.1 wire protocol — request parsing and response serialisation.
//!
//! Parsing uses `httparse` for the request line + headers (zero-alloc, zero-
//! copy, battle-tested). Body framing, keep-alive logic and response writing
//! are ours.
//!
//! v1 body framing:
//! - `Content-Length: N` — read exactly N bytes.
//! - No body header — empty body.
//! - `Transfer-Encoding: chunked` — deferred. Returns ParseError::Unsupported
//! and the connection actor responds 411 Length Required + close.
//!
//! Keep it stupid simple. Chunked decoding lands when something actually
//! requests it.
use crate::conn::{Body, Conn, HeaderMap, HttpVersion, Method, RespBody};
// ---------------------------------------------------------------------------
// ParseError
// ---------------------------------------------------------------------------
#[derive(Debug)]
pub enum ParseError {
/// Request bytes are well-formed but incomplete; need more from the
/// socket.
Incomplete,
/// Request is malformed; respond 400 and close.
Malformed,
/// Header count exceeded the configured max; respond 431 and close.
TooManyHeaders,
/// `Content-Length` header could not be parsed as an integer.
BadContentLength,
/// A wire feature we haven't implemented yet (e.g. chunked encoding).
/// Connection actor responds 411 + close.
Unsupported,
}
// ---------------------------------------------------------------------------
// ParsedHead — what `parse_head` returns on success.
// ---------------------------------------------------------------------------
//
// `head_len` is how many bytes from the start of the buffer the head
// occupied — the body starts at `&buf[head_len..]`.
pub struct ParsedHead {
pub head_len: usize,
pub method: Method,
pub path: String,
pub query: Option<String>,
pub version: HttpVersion,
pub headers: HeaderMap,
pub content_length: Option<usize>,
pub keep_alive: bool,
pub expect_100: bool,
}
/// Try to parse a request head from `buf`. Returns `Incomplete` if more
/// bytes are needed; the caller should read more and retry with the *same*
/// buffer.
pub fn parse_head(buf: &[u8], max_headers: usize) -> Result<ParsedHead, ParseError> {
// httparse needs a header array; size it from config. 64 is the spec
// default. Stack allocation; if a request has more than max_headers
// they get TooManyHeaders.
let mut header_buf = vec![httparse::EMPTY_HEADER; max_headers];
let mut req = httparse::Request::new(&mut header_buf);
let head_len = match req.parse(buf) {
Ok(httparse::Status::Complete(n)) => n,
Ok(httparse::Status::Partial) => return Err(ParseError::Incomplete),
Err(httparse::Error::TooManyHeaders) => return Err(ParseError::TooManyHeaders),
Err(_) => return Err(ParseError::Malformed),
};
let method_str = req.method.ok_or(ParseError::Malformed)?;
let raw_path = req.path.ok_or(ParseError::Malformed)?;
let v = req.version.ok_or(ParseError::Malformed)?;
let version = match v {
0 => HttpVersion::Http10,
1 => HttpVersion::Http11,
_ => return Err(ParseError::Malformed),
};
// Split path and query. We don't percent-decode the path here — the
// spec calls for it, but for v1 raw is fine; user code can decode if
// they need to. Same with the query.
let (path, query) = match raw_path.split_once('?') {
Some((p, q)) => (p.to_string(), Some(q.to_string())),
None => (raw_path.to_string(), None),
};
// Walk headers, building HeaderMap and pulling out the few we need
// ourselves (Content-Length, Connection, Transfer-Encoding, Expect).
let mut headers = HeaderMap::with_capacity(req.headers.len());
let mut content_length = None;
let mut connection_hdr = None;
let mut chunked = false;
let mut expect_100 = false;
for h in req.headers.iter() {
let name_lower = h.name.to_ascii_lowercase();
let value = std::str::from_utf8(h.value).map_err(|_| ParseError::Malformed)?;
match name_lower.as_str() {
"content-length" => {
content_length = Some(
value.trim()
.parse::<usize>()
.map_err(|_| ParseError::BadContentLength)?
);
}
"transfer-encoding" => {
// We only care whether it includes "chunked". Multiple codings
// can appear; chunked is the only one we'd need to decode.
if value.to_ascii_lowercase().split(',').any(|t| t.trim() == "chunked") {
chunked = true;
}
}
"connection" => {
connection_hdr = Some(value.to_ascii_lowercase());
}
"expect" => {
if value.eq_ignore_ascii_case("100-continue") {
expect_100 = true;
}
}
_ => {}
}
headers.append(&name_lower, value.to_string());
}
if chunked {
return Err(ParseError::Unsupported);
}
// Keep-alive logic, RFC 7230 §6.3:
// HTTP/1.1: keep-alive by default; "Connection: close" overrides.
// HTTP/1.0: close by default; "Connection: keep-alive" overrides.
let keep_alive = match version {
HttpVersion::Http11 => connection_hdr.as_deref() != Some("close"),
HttpVersion::Http10 => connection_hdr.as_deref() == Some("keep-alive"),
};
Ok(ParsedHead {
head_len,
method: Method::parse(method_str),
path,
query,
version,
headers,
content_length,
keep_alive,
expect_100,
})
}
// ---------------------------------------------------------------------------
// Conn assembly
// ---------------------------------------------------------------------------
pub fn build_conn(head: ParsedHead, body: Body) -> Conn {
let mut c = Conn::new();
c.method = head.method;
c.path = head.path;
c.query = head.query;
c.version = head.version;
c.headers = head.headers;
c.body = body;
c
}
// ---------------------------------------------------------------------------
// Response serialisation
// ---------------------------------------------------------------------------
//
// We emit:
// <version> <status> <reason>\r\n
// <header>: <value>\r\n
// ...
// \r\n
// <body bytes>
//
// Headers we always inject (unless the user already set them):
// - Content-Length: from resp_body.len_hint()
// - Connection: close (if keep-alive is off this request)
// - Date: skipped in v1; not required by the standard and adds complexity.
pub fn serialise_response(conn: &Conn, keep_alive: bool) -> Vec<u8> {
// Pre-size: status line ~30 + headers ~50/each + body. Good enough.
let body_len = conn.resp_body.len_hint();
let mut out = Vec::with_capacity(64 + conn.resp_headers.len() * 40 + body_len);
let status = conn.status.unwrap_or(200);
let reason = reason_phrase(status);
// Status line.
out.extend_from_slice(conn.version.as_str().as_bytes());
out.push(b' ');
out.extend_from_slice(status.to_string().as_bytes());
out.push(b' ');
out.extend_from_slice(reason.as_bytes());
out.extend_from_slice(b"\r\n");
// User headers — written first so subsequent injection can skip them.
let mut wrote_content_length = false;
let mut wrote_connection = false;
for (name, value) in conn.resp_headers.iter() {
match name {
"content-length" => wrote_content_length = true,
"connection" => wrote_connection = true,
_ => {}
}
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(b": ");
out.extend_from_slice(value.as_bytes());
out.extend_from_slice(b"\r\n");
}
if !wrote_content_length {
out.extend_from_slice(b"content-length: ");
out.extend_from_slice(body_len.to_string().as_bytes());
out.extend_from_slice(b"\r\n");
}
if !wrote_connection && !keep_alive {
out.extend_from_slice(b"connection: close\r\n");
}
out.extend_from_slice(b"\r\n");
// Body.
match &conn.resp_body {
RespBody::Empty => {}
RespBody::Bytes(b) => out.extend_from_slice(b),
}
out
}
/// HTTP/1.1 reason phrases. Required by RFC 7230 §3.1.2 — clients may
/// display them.
pub fn reason_phrase(status: u16) -> &'static str {
match status {
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "No Content",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
409 => "Conflict",
411 => "Length Required",
413 => "Payload Too Large",
414 => "URI Too Long",
415 => "Unsupported Media Type",
422 => "Unprocessable Entity",
429 => "Too Many Requests",
431 => "Request Header Fields Too Large",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
_ => "OK", // best-effort fallback; spec allows any phrase
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_get() {
let req = b"GET /hello HTTP/1.1\r\nHost: x\r\n\r\n";
let head = parse_head(req, 64).unwrap();
assert_eq!(head.method, Method::Get);
assert_eq!(head.path, "/hello");
assert_eq!(head.version, HttpVersion::Http11);
assert!(head.keep_alive);
assert_eq!(head.content_length, None);
}
#[test]
fn parse_with_query() {
let req = b"GET /users?id=42&active=true HTTP/1.1\r\nHost: x\r\n\r\n";
let head = parse_head(req, 64).unwrap();
assert_eq!(head.path, "/users");
assert_eq!(head.query.as_deref(), Some("id=42&active=true"));
}
#[test]
fn parse_post_with_content_length() {
let req = b"POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\nhello";
let head = parse_head(req, 64).unwrap();
assert_eq!(head.content_length, Some(5));
// head_len is bytes before the body; the remainder is the body.
assert_eq!(&req[head.head_len..], b"hello");
}
#[test]
fn parse_connection_close() {
let req = b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n";
let head = parse_head(req, 64).unwrap();
assert!(!head.keep_alive);
}
#[test]
fn parse_http10_default_close() {
let req = b"GET / HTTP/1.0\r\nHost: x\r\n\r\n";
let head = parse_head(req, 64).unwrap();
assert!(!head.keep_alive);
}
#[test]
fn parse_http10_keepalive_opt_in() {
let req = b"GET / HTTP/1.0\r\nHost: x\r\nConnection: keep-alive\r\n\r\n";
let head = parse_head(req, 64).unwrap();
assert!(head.keep_alive);
}
#[test]
fn parse_partial() {
let req = b"GET /hello HTTP/1.1\r\nHost: x\r\n"; // no terminator yet
match parse_head(req, 64) {
Err(ParseError::Incomplete) => {}
_ => panic!("expected Incomplete"),
}
}
#[test]
fn parse_chunked_unsupported() {
let req = b"POST /a HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n";
match parse_head(req, 64) {
Err(ParseError::Unsupported) => {}
_ => panic!("expected Unsupported for chunked"),
}
}
#[test]
fn serialise_basic_200() {
let conn = Conn::new().put_status(200).put_body("hi");
let bytes = serialise_response(&conn, true);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(s.contains("content-length: 2"));
assert!(s.ends_with("\r\n\r\nhi"));
}
#[test]
fn serialise_connection_close_when_not_keepalive() {
let conn = Conn::new().put_status(204);
let bytes = serialise_response(&conn, false);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.contains("connection: close"));
}
#[test]
fn serialise_user_content_length_is_respected() {
let conn = Conn::new().put_status(200)
.put_header("content-length", "999")
.put_body("hi"); // mismatch on purpose — user wins
let bytes = serialise_response(&conn, true);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.contains("content-length: 999"));
assert!(!s.contains("content-length: 2"));
}
}