//! 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, pub version: HttpVersion, pub headers: HeaderMap, pub content_length: Option, 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 { // 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::() .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: // \r\n //
: \r\n // ... // \r\n // // // 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 { // 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")); } }