Initial commit
This commit is contained in:
+379
@@ -0,0 +1,379 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user