Initial commit

This commit is contained in:
2026-05-26 23:16:45 +02:00
commit 3b6c466210
12 changed files with 2254 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
//! The connection actor — one per accepted TCP connection.
//!
//! Runs the HTTP/1.1 request loop:
//!
//! loop {
//! read bytes → parse → build Conn
//! pipeline.run(conn) // inline; no spawn
//! write response
//! if !keep_alive { break }
//! }
//!
//! Everything in here happens in one smarm green thread. The actor parks on
//! `wait_readable` between bytes and `wait_writable` during slow writes;
//! during those parks, other connection actors progress freely.
use crate::conn::{Body, Conn, RespBody};
use crate::net::OwnedFd;
use crate::parser::{self, ParseError};
use crate::plug::Pipeline;
use std::io::{self, ErrorKind};
use std::os::fd::RawFd;
use std::time::Duration;
// ---------------------------------------------------------------------------
// Limits
// ---------------------------------------------------------------------------
/// Per-connection settings the connection actor needs to honour.
#[derive(Clone, Copy, Debug)]
pub struct ConnLimits {
pub max_headers: usize,
pub initial_read_buf: usize,
/// Hard cap on the request head to bound buffer growth. 64 KiB is
/// well over Apache's 8 KiB default; protects against pathological
/// clients streaming headers forever.
pub max_head_bytes: usize,
/// Hard cap on Content-Length we'll accept. 16 MiB is enough for a CRUD
/// example; configurable in `Config`.
pub max_body_bytes: usize,
pub keep_alive_timeout: Duration,
}
impl Default for ConnLimits {
fn default() -> Self {
Self {
max_headers: 64,
initial_read_buf: 8 * 1024,
max_head_bytes: 64 * 1024,
max_body_bytes: 16 * 1024 * 1024,
keep_alive_timeout: Duration::from_secs(60),
}
}
}
// ---------------------------------------------------------------------------
// run_connection — entry point spawned by the listener actor.
// ---------------------------------------------------------------------------
pub fn run_connection(fd: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
// The OwnedFd cleans up via Drop on any exit path (panic, error, or
// normal close). No explicit close calls below.
let raw = fd.as_raw();
let mut buf: Vec<u8> = Vec::with_capacity(limits.initial_read_buf);
loop {
// ----- 1. Read until we have a full request head. -----
let parsed = match read_head(raw, &mut buf, &limits) {
Ok(p) => p,
Err(ReadHeadErr::ClientClosed) => {
// Clean EOF between requests (or before any request). Normal.
return;
}
Err(ReadHeadErr::Io(_)) => {
// Network error or timeout. Best-effort close; we're done.
return;
}
Err(ReadHeadErr::Parse(e)) => {
emit_error_response(raw, &e);
return;
}
};
// ----- 2. Read body. -----
let body_len = parsed.content_length.unwrap_or(0);
if body_len > limits.max_body_bytes {
let _ = write_all(raw, b"HTTP/1.1 413 Payload Too Large\r\ncontent-length: 0\r\nconnection: close\r\n\r\n");
return;
}
// If client sent `Expect: 100-continue`, emit it before reading the
// body. RFC 7231 §5.1.1. We don't gate on app logic here; v1 always
// accepts.
if parsed.expect_100 {
if write_all(raw, b"HTTP/1.1 100 Continue\r\n\r\n").is_err() {
return;
}
}
let body = match read_body(raw, &mut buf, parsed.head_len, body_len) {
Ok(b) => b,
Err(_) => return,
};
let keep_alive = parsed.keep_alive;
let version = parsed.version;
let head_len = parsed.head_len;
let conn = parser::build_conn(parsed, Body::from_bytes(body));
// ----- 3. Run the pipeline. -----
// Catch panics at the actor boundary — a panicking handler should
// not take down the whole connection silently with no response.
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
pipeline.run(conn)
}));
let mut response_conn = match result {
Ok(c) => c,
Err(_) => {
// Compose a 500 manually; the original Conn was moved into
// the closure.
let mut c = Conn::new();
c.version = version;
c.put_status(500).put_header("content-length", "0")
}
};
// If no plug touched status, that's a configuration error (no router
// matched, no default handler). Emit 404.
if response_conn.status.is_none() {
response_conn = response_conn.put_status(404)
.put_body(RespBody::Empty);
}
// ----- 4. Write the response. -----
let bytes = parser::serialise_response(&response_conn, keep_alive);
if write_all(raw, &bytes).is_err() {
return;
}
// ----- 5. Loop or close. -----
if !keep_alive {
return;
}
// Drop the request bytes (head + body) from `buf`; anything past
// them is the start of the next pipelined request.
let consumed = head_len + body_len;
buf.drain(..consumed);
}
}
// ---------------------------------------------------------------------------
// read_head
// ---------------------------------------------------------------------------
#[allow(dead_code)] // io::Error is captured for future logging
enum ReadHeadErr {
ClientClosed,
Io(io::Error),
Parse(ParseError),
}
/// Read until `parse_head` succeeds or fails definitively. `buf` may already
/// contain leftover bytes from a previous keep-alive cycle; we try to parse
/// those before reading more from the socket.
fn read_head(
fd: RawFd,
buf: &mut Vec<u8>,
limits: &ConnLimits,
) -> Result<parser::ParsedHead, ReadHeadErr> {
loop {
// Try to parse what we already have. On the first iteration of a
// fresh keep-alive cycle, `buf` may already hold the next request.
if !buf.is_empty() {
match parser::parse_head(buf, limits.max_headers) {
Ok(h) => return Ok(h),
Err(ParseError::Incomplete) => {} // need more bytes
Err(e) => return Err(ReadHeadErr::Parse(e)),
}
}
if buf.len() >= limits.max_head_bytes {
return Err(ReadHeadErr::Parse(ParseError::TooManyHeaders));
}
// Read more.
match read_some(fd, buf, limits.initial_read_buf) {
Ok(0) => return Err(ReadHeadErr::ClientClosed),
Ok(_) => continue,
Err(e) => return Err(ReadHeadErr::Io(e)),
}
}
}
// ---------------------------------------------------------------------------
// read_body
// ---------------------------------------------------------------------------
fn read_body(
fd: RawFd,
buf: &mut Vec<u8>,
head_len: usize,
body_len: usize,
) -> io::Result<Vec<u8>> {
// Bytes already in `buf` past the head belong to the body.
let already = buf.len().saturating_sub(head_len);
let need = body_len.saturating_sub(already);
if need == 0 {
// We have the full body in `buf` already. Extract a copy; `buf` is
// drained later in the connection loop.
return Ok(buf[head_len..head_len + body_len].to_vec());
}
// Read until we have the rest.
let mut total_read = already;
while total_read < body_len {
match read_some(fd, buf, 8 * 1024) {
Ok(0) => return Err(io::Error::new(ErrorKind::UnexpectedEof, "client closed during body")),
Ok(n) => total_read += n,
Err(e) => return Err(e),
}
}
Ok(buf[head_len..head_len + body_len].to_vec())
}
// ---------------------------------------------------------------------------
// read_some — single epoll-park + read loop.
// ---------------------------------------------------------------------------
//
// Appends what it reads onto `buf`. Returns bytes read, 0 for EOF, or the
// last io error.
fn read_some(fd: RawFd, buf: &mut Vec<u8>, chunk: usize) -> io::Result<usize> {
// Loop to absorb EAGAIN: a readable wakeup followed by EAGAIN is
// possible (signal race, etc). Re-park and retry rather than returning
// 0 (which would be confused with EOF by callers).
loop {
smarm::wait_readable(fd)?;
let start = buf.len();
buf.resize(start + chunk, 0);
let n = unsafe {
libc::read(fd, buf.as_mut_ptr().add(start) as *mut _, chunk)
};
if n < 0 {
let err = io::Error::last_os_error();
buf.truncate(start);
if err.kind() == ErrorKind::WouldBlock || err.kind() == ErrorKind::Interrupted {
continue;
}
return Err(err);
}
let n = n as usize;
buf.truncate(start + n);
return Ok(n); // n == 0 here is real EOF
}
}
// ---------------------------------------------------------------------------
// write_all — robust write loop.
// ---------------------------------------------------------------------------
fn write_all(fd: RawFd, mut buf: &[u8]) -> io::Result<()> {
while !buf.is_empty() {
// Park on writability before each syscall.
smarm::wait_writable(fd)?;
let n = unsafe {
libc::write(fd, buf.as_ptr() as *const _, buf.len())
};
if n < 0 {
let err = io::Error::last_os_error();
if err.kind() == ErrorKind::WouldBlock {
continue; // spurious wake; retry
}
return Err(err);
}
if n == 0 {
return Err(io::Error::new(ErrorKind::WriteZero, "write returned 0"));
}
buf = &buf[n as usize..];
}
Ok(())
}
// ---------------------------------------------------------------------------
// Error responses for unparseable / malformed requests.
// ---------------------------------------------------------------------------
fn emit_error_response(fd: RawFd, err: &ParseError) {
let resp: &[u8] = match err {
ParseError::TooManyHeaders =>
b"HTTP/1.1 431 Request Header Fields Too Large\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
ParseError::BadContentLength =>
b"HTTP/1.1 400 Bad Request\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
ParseError::Unsupported =>
b"HTTP/1.1 411 Length Required\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
// Incomplete and Malformed both lead here; Incomplete shouldn't
// appear (read_head loops on it).
_ =>
b"HTTP/1.1 400 Bad Request\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
};
let _ = write_all(fd, resp);
}