Initial commit
This commit is contained in:
+328
@@ -0,0 +1,328 @@
|
||||
//! `Conn` — the value that flows through the entire plug pipeline.
|
||||
//!
|
||||
//! Carries request data, response state being built, and arbitrary user
|
||||
//! data. Owned and moved at each step. Plugs are unaware of the underlying
|
||||
//! HTTP version (v1.1 today, /2 later).
|
||||
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Method
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Method {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Patch,
|
||||
Head,
|
||||
Options,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn parse(s: &str) -> Self {
|
||||
match s {
|
||||
"GET" => Method::Get,
|
||||
"POST" => Method::Post,
|
||||
"PUT" => Method::Put,
|
||||
"DELETE" => Method::Delete,
|
||||
"PATCH" => Method::Patch,
|
||||
"HEAD" => Method::Head,
|
||||
"OPTIONS" => Method::Options,
|
||||
_ => Method::Other(s.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Method::Get => "GET",
|
||||
Method::Post => "POST",
|
||||
Method::Put => "PUT",
|
||||
Method::Delete => "DELETE",
|
||||
Method::Patch => "PATCH",
|
||||
Method::Head => "HEAD",
|
||||
Method::Options => "OPTIONS",
|
||||
Method::Other(s) => s.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HttpVersion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HttpVersion {
|
||||
Http10,
|
||||
Http11,
|
||||
}
|
||||
|
||||
impl HttpVersion {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
HttpVersion::Http10 => "HTTP/1.0",
|
||||
HttpVersion::Http11 => "HTTP/1.1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HeaderMap — small-vec of (name, value) string pairs.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Per spec: most requests have <20 headers, so a linear-scan Vec is cheaper
|
||||
// than a hash map. Names are case-insensitive (HTTP/1.1), normalised to
|
||||
// lowercase on insert.
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HeaderMap {
|
||||
inner: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl HeaderMap {
|
||||
pub fn new() -> Self { Self::default() }
|
||||
|
||||
pub fn with_capacity(n: usize) -> Self {
|
||||
Self { inner: Vec::with_capacity(n) }
|
||||
}
|
||||
|
||||
/// Append a header. Does not deduplicate (HTTP allows repeats; e.g.
|
||||
/// `Set-Cookie`).
|
||||
pub fn append(&mut self, name: &str, value: impl Into<String>) {
|
||||
self.inner.push((name.to_ascii_lowercase(), value.into()));
|
||||
}
|
||||
|
||||
/// Replace any existing values for `name` with a single value.
|
||||
pub fn set(&mut self, name: &str, value: impl Into<String>) {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
self.inner.retain(|(n, _)| n != &lower);
|
||||
self.inner.push((lower, value.into()));
|
||||
}
|
||||
|
||||
/// First value for `name`, if any.
|
||||
pub fn get(&self, name: &str) -> Option<&str> {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
self.inner.iter()
|
||||
.find(|(n, _)| n == &lower)
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
/// Iterate over (name, value) pairs in insertion order.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.inner.iter().map(|(n, v)| (n.as_str(), v.as_str()))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize { self.inner.len() }
|
||||
pub fn is_empty(&self) -> bool { self.inner.is_empty() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Body — request body, exposed as owned bytes.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// v1: bodies are read in full by the connection actor before the pipeline
|
||||
// runs, when `Content-Length` is set. Streaming and chunked encoding will
|
||||
// come later; for now this is the simplest correct thing.
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Body {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Body {
|
||||
pub fn empty() -> Self { Self::default() }
|
||||
pub fn from_bytes(b: Vec<u8>) -> Self { Self { bytes: b } }
|
||||
pub fn as_bytes(&self) -> &[u8] { &self.bytes }
|
||||
pub fn into_bytes(self) -> Vec<u8> { self.bytes }
|
||||
pub fn len(&self) -> usize { self.bytes.len() }
|
||||
pub fn is_empty(&self) -> bool { self.bytes.is_empty() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RespBody — what plugs put on the wire.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Enum, not a trait, so the connection actor can pattern-match. Leaves room
|
||||
// for future variants like Sse, Stream, Chunked without touching the plug
|
||||
// API.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RespBody {
|
||||
Empty,
|
||||
Bytes(Vec<u8>),
|
||||
}
|
||||
|
||||
impl RespBody {
|
||||
pub fn len_hint(&self) -> usize {
|
||||
match self {
|
||||
RespBody::Empty => 0,
|
||||
RespBody::Bytes(b) => b.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RespBody {
|
||||
fn default() -> Self { RespBody::Empty }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Params — path parameters extracted by the router.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Small-vec semantics: routes have a handful of params; a Vec is the right
|
||||
// shape. Empty until the router plug populates it.
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Params {
|
||||
inner: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub fn new() -> Self { Self::default() }
|
||||
|
||||
pub fn put(&mut self, name: impl Into<String>, value: impl Into<String>) {
|
||||
self.inner.push((name.into(), value.into()));
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&str> {
|
||||
self.inner.iter()
|
||||
.find(|(n, _)| n == name)
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.inner.iter().map(|(n, v)| (n.as_str(), v.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Assigns — type-erased map for arbitrary user data.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Same pattern as Phoenix's `conn.assigns`. The `Option<HashMap>` keeps the
|
||||
// common case (no assigns) at zero allocation cost.
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Assigns {
|
||||
inner: Option<HashMap<TypeId, Box<dyn Any + Send>>>,
|
||||
}
|
||||
|
||||
impl Assigns {
|
||||
pub fn new() -> Self { Self::default() }
|
||||
|
||||
pub fn put<T: Any + Send>(&mut self, value: T) {
|
||||
self.inner
|
||||
.get_or_insert_with(HashMap::new)
|
||||
.insert(TypeId::of::<T>(), Box::new(value));
|
||||
}
|
||||
|
||||
pub fn get<T: Any + Send>(&self) -> Option<&T> {
|
||||
self.inner
|
||||
.as_ref()?
|
||||
.get(&TypeId::of::<T>())?
|
||||
.downcast_ref::<T>()
|
||||
}
|
||||
|
||||
pub fn take<T: Any + Send>(&mut self) -> Option<T> {
|
||||
let boxed = self.inner.as_mut()?.remove(&TypeId::of::<T>())?;
|
||||
boxed.downcast::<T>().ok().map(|b| *b)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conn — the pipeline value.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Conn {
|
||||
// Request (populated before the pipeline runs).
|
||||
pub method: Method,
|
||||
pub path: String,
|
||||
pub query: Option<String>,
|
||||
pub version: HttpVersion,
|
||||
pub headers: HeaderMap,
|
||||
pub body: Body,
|
||||
|
||||
// Response (built up by plugs, written after the pipeline returns).
|
||||
pub status: Option<u16>,
|
||||
pub resp_headers: HeaderMap,
|
||||
pub resp_body: RespBody,
|
||||
|
||||
// Pipeline state.
|
||||
pub params: Params,
|
||||
pub assigns: Assigns,
|
||||
pub halted: bool,
|
||||
}
|
||||
|
||||
impl Conn {
|
||||
/// Construct an empty `Conn`. Tests use this; the connection actor uses
|
||||
/// `From<RequestParts>` (see parser.rs).
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
method: Method::Get,
|
||||
path: String::new(),
|
||||
query: None,
|
||||
version: HttpVersion::Http11,
|
||||
headers: HeaderMap::new(),
|
||||
body: Body::empty(),
|
||||
|
||||
status: None,
|
||||
resp_headers: HeaderMap::new(),
|
||||
resp_body: RespBody::Empty,
|
||||
|
||||
params: Params::new(),
|
||||
assigns: Assigns::new(),
|
||||
halted: false,
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Fluent builders. Most handler code uses these. -----
|
||||
|
||||
pub fn put_status(mut self, status: u16) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn put_header(mut self, name: &str, value: impl Into<String>) -> Self {
|
||||
self.resp_headers.set(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn put_body(mut self, body: impl Into<RespBody>) -> Self {
|
||||
self.resp_body = body.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn put_params(mut self, params: Params) -> Self {
|
||||
self.params = params;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn halt(mut self) -> Self {
|
||||
self.halted = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Conn {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
// ----- Ergonomic conversions into RespBody. -----
|
||||
|
||||
impl From<Vec<u8>> for RespBody {
|
||||
fn from(v: Vec<u8>) -> Self { RespBody::Bytes(v) }
|
||||
}
|
||||
impl From<String> for RespBody {
|
||||
fn from(s: String) -> Self { RespBody::Bytes(s.into_bytes()) }
|
||||
}
|
||||
impl From<&'static str> for RespBody {
|
||||
fn from(s: &'static str) -> Self { RespBody::Bytes(s.as_bytes().to_vec()) }
|
||||
}
|
||||
impl From<&[u8]> for RespBody {
|
||||
fn from(s: &[u8]) -> Self { RespBody::Bytes(s.to_vec()) }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//! # urus — a cowboy/bandit-style HTTP library for the smarm actor runtime.
|
||||
//!
|
||||
//! v1 covers HTTP/1.1, the plug pipeline, and a built-in router. See
|
||||
//! `urus-spec.md` for the design.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use urus::{Pipeline, Router, Conn, Next, serve};
|
||||
//!
|
||||
//! let pipeline = Pipeline::new().plug(
|
||||
//! Router::new()
|
||||
//! .get("/", |c: Conn, _n: Next| c.put_status(200).put_body("hello"))
|
||||
//! .get("/users/:id", |c: Conn, _n: Next| {
|
||||
//! let id = c.params.get("id").unwrap_or("").to_string();
|
||||
//! c.put_status(200).put_body(id)
|
||||
//! })
|
||||
//! );
|
||||
//!
|
||||
//! serve("0.0.0.0:8080", pipeline).unwrap();
|
||||
//! ```
|
||||
|
||||
pub mod conn;
|
||||
pub mod plug;
|
||||
pub mod router;
|
||||
pub mod parser;
|
||||
pub mod net;
|
||||
pub mod conn_actor;
|
||||
pub mod serve;
|
||||
|
||||
// Re-exports — what most users want at the crate root.
|
||||
pub use conn::{Assigns, Body, Conn, HeaderMap, HttpVersion, Method, Params, RespBody};
|
||||
pub use plug::{Next, Pipeline, Plug};
|
||||
pub use router::Router;
|
||||
pub use serve::{serve, serve_with, Config};
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
//! Thin TCP socket layer.
|
||||
//!
|
||||
//! We don't use `std::net::TcpListener` because we need non-blocking accept
|
||||
//! integrated with smarm's epoll loop. So this drops down to libc: socket,
|
||||
//! bind, listen, accept4. All fds carry `O_NONBLOCK | O_CLOEXEC` so smarm's
|
||||
//! readiness primitives work as documented.
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OwnedFd — a tiny RAII wrapper that closes on drop.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Sent through smarm channels (which require `Send`) for zero-copy fd
|
||||
// handoff between listener and connection actor. `Send` is safe: fd values
|
||||
// are just integers, and ownership semantics — exactly-one closer — are
|
||||
// enforced by the type itself.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OwnedFd {
|
||||
fd: RawFd,
|
||||
}
|
||||
|
||||
impl OwnedFd {
|
||||
/// Wrap a raw fd. The wrapper now owns the fd and will close it on drop.
|
||||
pub fn from_raw(fd: RawFd) -> Self {
|
||||
Self { fd }
|
||||
}
|
||||
|
||||
pub fn as_raw(&self) -> RawFd { self.fd }
|
||||
|
||||
/// Release ownership without closing. The caller must close the fd.
|
||||
pub fn into_raw(self) -> RawFd {
|
||||
let fd = self.fd;
|
||||
std::mem::forget(self);
|
||||
fd
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OwnedFd {
|
||||
fn drop(&mut self) {
|
||||
if self.fd >= 0 {
|
||||
unsafe { libc::close(self.fd); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fd handoff: SAFETY: RawFd is an integer; sending one across threads
|
||||
// transfers ownership in the same way moving an i32 would. The recipient
|
||||
// becomes the unique closer.
|
||||
unsafe impl Send for OwnedFd {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bind_and_listen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LISTEN_BACKLOG: i32 = 1024;
|
||||
|
||||
/// Create a non-blocking, SO_REUSEADDR TCP listener bound to `addr`. The
|
||||
/// returned fd is ready for `accept4` calls; the caller registers it with
|
||||
/// smarm via `wait_readable` between accepts.
|
||||
pub fn bind_and_listen(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
let family = match addr {
|
||||
SocketAddr::V4(_) => libc::AF_INET,
|
||||
SocketAddr::V6(_) => libc::AF_INET6,
|
||||
};
|
||||
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
family,
|
||||
libc::SOCK_STREAM | libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let owned = OwnedFd::from_raw(fd);
|
||||
|
||||
// SO_REUSEADDR — standard for servers; avoids TIME_WAIT bind failures
|
||||
// on restart.
|
||||
let opt: libc::c_int = 1;
|
||||
let r = unsafe {
|
||||
libc::setsockopt(
|
||||
fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_REUSEADDR,
|
||||
&opt as *const _ as *const libc::c_void,
|
||||
std::mem::size_of_val(&opt) as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if r < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// bind().
|
||||
match addr {
|
||||
SocketAddr::V4(a) => {
|
||||
let sa = libc::sockaddr_in {
|
||||
sin_family: libc::AF_INET as u16,
|
||||
sin_port: a.port().to_be(),
|
||||
sin_addr: libc::in_addr { s_addr: u32::from_ne_bytes(a.ip().octets()) },
|
||||
sin_zero: [0; 8],
|
||||
};
|
||||
let r = unsafe {
|
||||
libc::bind(
|
||||
fd,
|
||||
&sa as *const _ as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if r < 0 { return Err(io::Error::last_os_error()); }
|
||||
}
|
||||
SocketAddr::V6(a) => {
|
||||
let sa = libc::sockaddr_in6 {
|
||||
sin6_family: libc::AF_INET6 as u16,
|
||||
sin6_port: a.port().to_be(),
|
||||
sin6_flowinfo: a.flowinfo(),
|
||||
sin6_addr: libc::in6_addr { s6_addr: a.ip().octets() },
|
||||
sin6_scope_id: a.scope_id(),
|
||||
};
|
||||
let r = unsafe {
|
||||
libc::bind(
|
||||
fd,
|
||||
&sa as *const _ as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if r < 0 { return Err(io::Error::last_os_error()); }
|
||||
}
|
||||
}
|
||||
|
||||
// listen().
|
||||
let r = unsafe { libc::listen(fd, LISTEN_BACKLOG) };
|
||||
if r < 0 { return Err(io::Error::last_os_error()); }
|
||||
|
||||
Ok(owned)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// accept_nonblocking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One non-blocking `accept4`. Returns the new fd on success,
|
||||
/// `Err(WouldBlock)` if no connection is pending (caller should park on
|
||||
/// `wait_readable(listener)` and retry), or other errors directly.
|
||||
pub fn accept_nonblocking(listener: RawFd) -> io::Result<OwnedFd> {
|
||||
let mut addr: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
|
||||
let mut len: libc::socklen_t = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||
|
||||
let fd = unsafe {
|
||||
libc::accept4(
|
||||
listener,
|
||||
&mut addr as *mut _ as *mut libc::sockaddr,
|
||||
&mut len,
|
||||
libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(OwnedFd::from_raw(fd))
|
||||
}
|
||||
+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"));
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
//! The plug pipeline — universal abstraction for everything in urus.
|
||||
//!
|
||||
//! A `Plug` is anything that takes a `Conn`, optionally calls `next.run(conn)`
|
||||
//! to delegate to the rest of the pipeline, and returns a `Conn`. Routing,
|
||||
//! middleware, handlers — all the same shape.
|
||||
//!
|
||||
//! The halt is structural: not calling `next.run(conn)` *is* the halt. Setting
|
||||
//! `conn.halted = true` is only a hint to outer plugs that the pipeline was
|
||||
//! short-circuited (e.g. a logger that wraps `next`).
|
||||
|
||||
use crate::conn::Conn;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plug trait
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub trait Plug: Send + Sync + 'static {
|
||||
fn call(&self, conn: Conn, next: Next) -> Conn;
|
||||
}
|
||||
|
||||
// Blanket impl for closures. Both `Send + Sync` so plug values can be shared
|
||||
// across connection actors via the Arc'd pipeline.
|
||||
impl<F> Plug for F
|
||||
where
|
||||
F: Fn(Conn, Next) -> Conn + Send + Sync + 'static,
|
||||
{
|
||||
fn call(&self, conn: Conn, next: Next) -> Conn {
|
||||
self(conn, next)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Next — a handle to the remainder of the pipeline.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Opaque wrapper around a slice of the remaining plugs. `run(conn)` consumes
|
||||
// the handle (no double-calling) and dispatches to the head plug. Empty
|
||||
// remainder is a no-op — the conn passes through unchanged.
|
||||
|
||||
pub struct Next<'a> {
|
||||
plugs: &'a [Arc<dyn Plug>],
|
||||
}
|
||||
|
||||
impl<'a> Next<'a> {
|
||||
pub(crate) fn new(plugs: &'a [Arc<dyn Plug>]) -> Self {
|
||||
Self { plugs }
|
||||
}
|
||||
|
||||
/// Continue down the pipeline. Returns the `Conn` produced by the rest.
|
||||
pub fn run(self, conn: Conn) -> Conn {
|
||||
match self.plugs.split_first() {
|
||||
None => conn,
|
||||
Some((head, tail)) => head.call(conn, Next { plugs: tail }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipeline
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Built once at startup, cloned cheaply (Arc bump) into each connection
|
||||
// actor. Read-only after construction; no per-request allocation in here.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Pipeline {
|
||||
plugs: Arc<Vec<Arc<dyn Plug>>>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new() -> Self {
|
||||
Self { plugs: Arc::new(Vec::new()) }
|
||||
}
|
||||
|
||||
/// Append a plug. Builder pattern — consumes and returns self.
|
||||
///
|
||||
/// Mutates the underlying Vec via `Arc::make_mut` when possible (it's
|
||||
/// unique during construction), which is allocation-free after the first
|
||||
/// `plug()` until clones are taken.
|
||||
pub fn plug(mut self, p: impl Plug) -> Self {
|
||||
let v = Arc::make_mut(&mut self.plugs);
|
||||
v.push(Arc::new(p));
|
||||
self
|
||||
}
|
||||
|
||||
/// Run the pipeline against a `Conn`. The connection actor calls this
|
||||
/// once per request.
|
||||
pub fn run(&self, conn: Conn) -> Conn {
|
||||
Next::new(self.plugs.as_slice()).run(conn)
|
||||
}
|
||||
|
||||
/// Number of plugs in the pipeline. Mostly for tests.
|
||||
pub fn len(&self) -> usize { self.plugs.len() }
|
||||
pub fn is_empty(&self) -> bool { self.plugs.is_empty() }
|
||||
}
|
||||
|
||||
impl Default for Pipeline {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
//! The router — itself a plug.
|
||||
//!
|
||||
//! v1: linear scan over a Vec of compiled route patterns. The spec calls for
|
||||
//! a radix trie eventually; for "good enough to build a CRUD app" with
|
||||
//! typical route counts (<50), the linear scan is dominant noise next to
|
||||
//! everything else on the request path. Replace with a trie when there's
|
||||
//! evidence it matters.
|
||||
//!
|
||||
//! Pattern syntax:
|
||||
//! - `/users` — literal
|
||||
//! - `/users/:id` — `id` param
|
||||
//! - `/users/:id/posts/:n` — multiple params
|
||||
//!
|
||||
//! No wildcards in v1. They are not needed for CRUD.
|
||||
|
||||
use crate::conn::{Conn, Method, Params};
|
||||
use crate::plug::{Next, Plug};
|
||||
use std::sync::Arc;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pattern
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A compiled URL pattern: a list of segments, each either a literal or a
|
||||
/// named parameter. Segment compilation happens at `Router::get(...)` time;
|
||||
/// matching at request time is a single-pass loop.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Pattern {
|
||||
segments: Vec<Segment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Segment {
|
||||
Literal(String),
|
||||
Param(String),
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
fn compile(path: &str) -> Self {
|
||||
// Treat empty path the same as "/" — single empty segment.
|
||||
let trimmed = path.trim_start_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
return Pattern { segments: Vec::new() };
|
||||
}
|
||||
let segments = trimmed
|
||||
.split('/')
|
||||
.map(|s| {
|
||||
if let Some(name) = s.strip_prefix(':') {
|
||||
Segment::Param(name.to_string())
|
||||
} else {
|
||||
Segment::Literal(s.to_string())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Pattern { segments }
|
||||
}
|
||||
|
||||
/// Match `path` against this pattern. Returns extracted params on
|
||||
/// success, `None` on miss. Strict path equality — trailing slashes
|
||||
/// matter; that's standard HTTP behaviour and we don't paper over it.
|
||||
fn match_path(&self, path: &str) -> Option<Params> {
|
||||
let trimmed = path.trim_start_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
return if self.segments.is_empty() { Some(Params::new()) } else { None };
|
||||
}
|
||||
let parts: Vec<&str> = trimmed.split('/').collect();
|
||||
if parts.len() != self.segments.len() {
|
||||
return None;
|
||||
}
|
||||
let mut params = Params::new();
|
||||
for (seg, part) in self.segments.iter().zip(parts.iter()) {
|
||||
match seg {
|
||||
Segment::Literal(lit) => {
|
||||
if lit != part {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Segment::Param(name) => {
|
||||
params.put(name.clone(), (*part).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(params)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Route {
|
||||
method: Method,
|
||||
pattern: Pattern,
|
||||
handler: Arc<dyn Plug>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Router
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct Router {
|
||||
routes: Vec<Route>,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
pub fn new() -> Self {
|
||||
Self { routes: Vec::new() }
|
||||
}
|
||||
|
||||
fn add(mut self, method: Method, path: &str, plug: impl Plug) -> Self {
|
||||
self.routes.push(Route {
|
||||
method,
|
||||
pattern: Pattern::compile(path),
|
||||
handler: Arc::new(plug),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Get, path, plug) }
|
||||
pub fn post(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Post, path, plug) }
|
||||
pub fn put(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Put, path, plug) }
|
||||
pub fn delete(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Delete, path, plug) }
|
||||
pub fn patch(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Patch, path, plug) }
|
||||
pub fn head(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Head, path, plug) }
|
||||
pub fn options(self, path: &str, plug: impl Plug) -> Self { self.add(Method::Options, path, plug) }
|
||||
}
|
||||
|
||||
impl Default for Router {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
impl Plug for Router {
|
||||
fn call(&self, conn: Conn, next: Next) -> Conn {
|
||||
// Two-pass dispatch: first match on method+path; if no method-and-
|
||||
// path matches but a same-path-different-method does, return 405.
|
||||
// Otherwise pass through to `next` so outer pipelines can layer a
|
||||
// 404 handler (or skip and let the connection actor emit a default).
|
||||
let mut path_seen = false;
|
||||
for route in &self.routes {
|
||||
if let Some(params) = route.pattern.match_path(&conn.path) {
|
||||
if route.method == conn.method {
|
||||
let conn = conn.put_params(params);
|
||||
return route.handler.call(conn, next);
|
||||
}
|
||||
path_seen = true;
|
||||
}
|
||||
}
|
||||
if path_seen {
|
||||
// Path is known, method isn't — RFC 7231 §6.5.5.
|
||||
conn.put_status(405)
|
||||
.put_header("content-length", "0")
|
||||
.halt()
|
||||
} else {
|
||||
// No route matched at all; let outer plugs / default handler
|
||||
// deal with it.
|
||||
next.run(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::plug::Pipeline;
|
||||
|
||||
fn ok_handler(_label: &'static str) -> impl Plug {
|
||||
move |conn: Conn, _next: Next| conn.put_status(200).put_body(_label)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_match() {
|
||||
let r = Router::new().get("/ping", ok_handler("pong"));
|
||||
let mut c = Conn::new();
|
||||
c.method = Method::Get;
|
||||
c.path = "/ping".into();
|
||||
let out = Pipeline::new().plug(r).run(c);
|
||||
assert_eq!(out.status, Some(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_match() {
|
||||
let r = Router::new().get("/users/:id", |conn: Conn, _n: Next| {
|
||||
let id = conn.params.get("id").unwrap_or("").to_string();
|
||||
conn.put_status(200).put_body(id)
|
||||
});
|
||||
let mut c = Conn::new();
|
||||
c.method = Method::Get;
|
||||
c.path = "/users/42".into();
|
||||
let out = Pipeline::new().plug(r).run(c);
|
||||
assert_eq!(out.status, Some(200));
|
||||
match &out.resp_body {
|
||||
crate::conn::RespBody::Bytes(b) => assert_eq!(b, b"42"),
|
||||
_ => panic!("expected bytes"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_mismatch_yields_405() {
|
||||
let r = Router::new().get("/users", ok_handler("list"));
|
||||
let mut c = Conn::new();
|
||||
c.method = Method::Post;
|
||||
c.path = "/users".into();
|
||||
let out = Pipeline::new().plug(r).run(c);
|
||||
assert_eq!(out.status, Some(405));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_route_falls_through() {
|
||||
// Router returns conn unchanged; next plug (a 404 default) handles it.
|
||||
let r = Router::new().get("/users", ok_handler("list"));
|
||||
let default_404 = |conn: Conn, _n: Next|
|
||||
conn.put_status(404).put_body("not found");
|
||||
let mut c = Conn::new();
|
||||
c.method = Method::Get;
|
||||
c.path = "/nowhere".into();
|
||||
let out = Pipeline::new().plug(r).plug(default_404).run(c);
|
||||
assert_eq!(out.status, Some(404));
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
//! Listener pool and the `serve` entry point.
|
||||
//!
|
||||
//! A small fixed pool of listener actors share the same TCP listen fd (via
|
||||
//! `dup`) — each blocks in non-blocking `accept4` + `wait_readable` on its
|
||||
//! own copy. When a connection arrives the listener spawns a connection
|
||||
//! actor with the `OwnedFd` and immediately returns to `accept`. No
|
||||
//! coordination needed; the kernel serialises `accept` calls across the fds.
|
||||
//!
|
||||
//! Sharing via `dup` rather than the same fd is deliberate — Linux's
|
||||
//! `accept4` is thread-safe on a single fd, but dup'ing per-listener keeps
|
||||
//! each actor's epoll registration local to its own RawFd value (so smarm's
|
||||
//! `waiters: HashMap<RawFd, Pid>` doesn't see collisions between listeners
|
||||
//! waiting on "the same fd").
|
||||
|
||||
use crate::conn_actor::{run_connection, ConnLimits};
|
||||
use crate::net::{accept_nonblocking, bind_and_listen, OwnedFd};
|
||||
use crate::plug::Pipeline;
|
||||
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::os::fd::RawFd;
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub addr: SocketAddr,
|
||||
pub listener_pool: usize,
|
||||
pub keep_alive_timeout: Duration,
|
||||
pub max_header_count: usize,
|
||||
pub read_buf_size: usize,
|
||||
pub request_timeout: Duration,
|
||||
pub max_body_bytes: usize,
|
||||
/// Number of smarm scheduler OS threads. `None` means smarm's default
|
||||
/// (one per CPU). Set this to a small fixed number in tests so multiple
|
||||
/// concurrent test servers don't oversubscribe the host.
|
||||
pub scheduler_threads: Option<usize>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(addr: SocketAddr) -> Self {
|
||||
let pool = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(2)
|
||||
.max(2);
|
||||
Self {
|
||||
addr,
|
||||
listener_pool: pool,
|
||||
keep_alive_timeout: Duration::from_secs(60),
|
||||
max_header_count: 64,
|
||||
read_buf_size: 8 * 1024,
|
||||
request_timeout: Duration::from_secs(30),
|
||||
max_body_bytes: 16 * 1024 * 1024,
|
||||
scheduler_threads: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_conn_limits(&self) -> ConnLimits {
|
||||
ConnLimits {
|
||||
max_headers: self.max_header_count,
|
||||
initial_read_buf: self.read_buf_size,
|
||||
max_head_bytes: 64 * 1024,
|
||||
max_body_bytes: self.max_body_bytes,
|
||||
keep_alive_timeout: self.keep_alive_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// dup helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn dup_fd(fd: RawFd) -> io::Result<OwnedFd> {
|
||||
let new_fd = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
|
||||
if new_fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(OwnedFd::from_raw(new_fd))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// listener actor body
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn listener_loop(listener: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
let fd = listener.as_raw();
|
||||
loop {
|
||||
match accept_nonblocking(fd) {
|
||||
Ok(client) => {
|
||||
// Hand the fd off to a new connection actor. spawn() is
|
||||
// cheap on smarm — it's a single Vec push under the
|
||||
// shared lock.
|
||||
let p = pipeline.clone();
|
||||
let l = limits;
|
||||
smarm::spawn(move || run_connection(client, p, l));
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
// No pending connection. Park until the listener is
|
||||
// readable again, then retry.
|
||||
if let Err(we) = smarm::wait_readable(fd) {
|
||||
// epoll registration failed — fatal for this listener.
|
||||
eprintln!("urus: listener wait_readable failed: {we}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
// EMFILE / ENFILE / ECONNABORTED etc. Log and continue;
|
||||
// the system may recover.
|
||||
eprintln!("urus: accept error: {e}");
|
||||
// Small backoff via smarm's sleep to avoid spinning if
|
||||
// the error is sticky.
|
||||
smarm::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
// listener OwnedFd drops here, closing the dup'd fd.
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// serve_with — main entry. Boots smarm, spawns listeners, blocks.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Boots an smarm runtime (one OS thread per CPU by default — see smarm's
|
||||
// `Config::default()`) and runs until externally killed. We don't wire a
|
||||
// graceful-shutdown signal in v1; the runtime exits when all actors exit,
|
||||
// which they don't (listener loops are infinite). Ctrl-C is your friend.
|
||||
|
||||
pub fn serve_with(config: Config, pipeline: Pipeline) -> io::Result<()> {
|
||||
let listener = bind_and_listen(config.addr)?;
|
||||
println!("urus: listening on {}", config.addr);
|
||||
|
||||
// We want one connection-actor-spawning loop per listener pool slot.
|
||||
// Each gets its own dup'd fd so epoll registrations don't collide.
|
||||
let mut listener_fds = Vec::with_capacity(config.listener_pool);
|
||||
listener_fds.push(listener); // primary keeps the original
|
||||
|
||||
for _ in 1..config.listener_pool {
|
||||
let dup = dup_fd(listener_fds[0].as_raw())?;
|
||||
listener_fds.push(dup);
|
||||
}
|
||||
|
||||
let limits = config.to_conn_limits();
|
||||
|
||||
// smarm's runtime API: init(Config) then run(f). The closure is the
|
||||
// root actor; from there we spawn one listener per fd in the pool.
|
||||
let smarm_cfg = match config.scheduler_threads {
|
||||
Some(n) => smarm::Config::exact(n),
|
||||
None => smarm::Config::default(),
|
||||
};
|
||||
let rt = smarm::init(smarm_cfg);
|
||||
rt.run(move || {
|
||||
let n = listener_fds.len();
|
||||
let mut handles = Vec::with_capacity(n);
|
||||
for (i, lfd) in listener_fds.into_iter().enumerate() {
|
||||
let p = pipeline.clone();
|
||||
let h = smarm::spawn(move || {
|
||||
println!("urus: listener {} starting", i);
|
||||
listener_loop(lfd, p, limits);
|
||||
});
|
||||
handles.push(h);
|
||||
}
|
||||
// Block forever (until ctrl-C) by joining the listeners. They
|
||||
// never exit on their own in v1.
|
||||
for h in handles {
|
||||
let _ = h.join();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// serve — convenience over serve_with.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn serve(addr: impl ToSocketAddrs, pipeline: Pipeline) -> io::Result<()> {
|
||||
let addr = addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "no addresses resolved"))?;
|
||||
serve_with(Config::new(addr), pipeline)
|
||||
}
|
||||
Reference in New Issue
Block a user