use sim_cancel::{Cancellation, CancellationReason};
use sim_kernel::{Error, Result};
use std::{io, sync::Mutex, time::Duration};
pub type Header = (String, String);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestHead {
pub method: String,
pub target: String,
pub headers: Vec<Header>,
pub peer: Option<String>,
pub local: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResponseHead {
pub status: u16,
pub headers: Vec<Header>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BodyLimits {
pub max_request_bytes: usize,
pub max_chunk_bytes: usize,
}
impl BodyLimits {
fn validate(self) -> Result<Self> {
if self.max_request_bytes == 0 || self.max_chunk_bytes == 0 {
return Err(Error::Eval("raw HTTP body limits must be non-zero".into()));
}
Ok(self)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TrailersPolicy {
Deny,
Allow,
}
#[derive(Clone, Debug)]
pub struct RequestScope {
cancellation: Cancellation,
deadline: Duration,
}
impl RequestScope {
#[must_use]
pub fn child(parent: &Cancellation, deadline: Duration) -> Self {
Self {
cancellation: parent.child(),
deadline,
}
}
#[must_use]
pub fn cancellation(&self) -> &Cancellation {
&self.cancellation
}
#[must_use]
pub fn deadline(&self) -> Duration {
self.deadline
}
pub fn cancel_timeout(&self) {
self.cancel("request deadline reached");
}
pub fn cancel_peer_drop(&self) {
self.cancel("peer disconnected");
}
fn cancel(&self, reason: &'static str) {
self.cancellation
.cancel(CancellationReason::new(reason).expect("static reason is valid"));
}
}
pub trait BodyReader {
fn next_chunk(&mut self, scope: &RequestScope) -> io::Result<Option<Vec<u8>>>;
}
pub trait ResponseWriter {
fn write_head(&mut self, head: ResponseHead, scope: &RequestScope) -> io::Result<()>;
fn write_chunk(&mut self, chunk: &[u8], scope: &RequestScope) -> io::Result<()>;
fn finish(&mut self, trailers: &[Header], scope: &RequestScope) -> io::Result<()>;
}
pub trait RawConnection {
fn parts(&mut self) -> (&RequestHead, &mut dyn BodyReader, &mut dyn ResponseWriter);
}
pub trait RawHandler: Send + Sync {
fn handle(
&self,
head: &RequestHead,
body: &mut dyn BodyReader,
response: &mut dyn ResponseWriter,
scope: &RequestScope,
) -> Result<()>;
}
pub struct RawHttpServer<H> {
handler: H,
limits: BodyLimits,
trailers: TrailersPolicy,
request_deadline: Duration,
shutdown: Cancellation,
active: Mutex<Vec<Cancellation>>,
}
impl<H: RawHandler> RawHttpServer<H> {
pub fn new(
handler: H,
limits: BodyLimits,
trailers: TrailersPolicy,
request_deadline: Duration,
) -> Result<Self> {
if request_deadline.is_zero() {
return Err(Error::Eval(
"raw HTTP request deadline must be non-zero".into(),
));
}
Ok(Self {
handler,
limits: limits.validate()?,
trailers,
request_deadline,
shutdown: Cancellation::new(),
active: Mutex::new(Vec::new()),
})
}
pub fn shutdown(&self) {
self.shutdown
.cancel(CancellationReason::new("server shutdown").expect("static reason is valid"));
for request in self
.active
.lock()
.expect("active request mutex poisoned")
.drain(..)
{
request.cancel(
CancellationReason::new("server shutdown").expect("static reason is valid"),
);
}
}
pub fn serve(&self, connection: &mut dyn RawConnection, caller: &Cancellation) -> Result<()> {
let scope = RequestScope::child(caller, self.request_deadline);
if self.shutdown.is_cancelled() {
scope.cancel("server shutdown");
}
self.active
.lock()
.expect("active request mutex poisoned")
.push(scope.cancellation.clone());
let (head, body, response) = connection.parts();
let head = head.clone();
let mut body = LimitedBody {
inner: body,
limits: self.limits,
received: 0,
};
let body: &mut dyn BodyReader = &mut body;
let mut response = LimitedResponse {
inner: response,
max_chunk: self.limits.max_chunk_bytes,
trailers: self.trailers,
};
let result = self.handler.handle(&head, body, &mut response, &scope);
if result.is_err() {
scope.cancel("handler failure");
}
scope.cancel("request complete");
self.active
.lock()
.expect("active request mutex poisoned")
.retain(|request| !request.is_cancelled());
result
}
}
struct LimitedBody<'a> {
inner: &'a mut dyn BodyReader,
limits: BodyLimits,
received: usize,
}
impl BodyReader for LimitedBody<'_> {
fn next_chunk(&mut self, scope: &RequestScope) -> io::Result<Option<Vec<u8>>> {
if scope.cancellation().is_cancelled() {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"request cancelled",
));
}
let chunk = self
.inner
.next_chunk(scope)
.inspect_err(|_| scope.cancel_peer_drop())?;
if let Some(chunk) = &chunk {
if chunk.is_empty()
|| chunk.len() > self.limits.max_chunk_bytes
|| self.received.saturating_add(chunk.len()) > self.limits.max_request_bytes
{
scope.cancel("request body cap exceeded");
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"request body cap exceeded",
));
}
self.received += chunk.len();
}
Ok(chunk)
}
}
struct LimitedResponse<'a> {
inner: &'a mut dyn ResponseWriter,
max_chunk: usize,
trailers: TrailersPolicy,
}
impl ResponseWriter for LimitedResponse<'_> {
fn write_head(&mut self, head: ResponseHead, scope: &RequestScope) -> io::Result<()> {
self.inner
.write_head(head, scope)
.inspect_err(|_| scope.cancel("response write failure"))
}
fn write_chunk(&mut self, chunk: &[u8], scope: &RequestScope) -> io::Result<()> {
if chunk.is_empty() || chunk.len() > self.max_chunk {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"response chunk outside bounds",
));
}
self.inner
.write_chunk(chunk, scope)
.inspect_err(|_| scope.cancel("response write failure"))
}
fn finish(&mut self, trailers: &[Header], scope: &RequestScope) -> io::Result<()> {
if !trailers.is_empty() && self.trailers == TrailersPolicy::Deny {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"response trailers denied",
));
}
self.inner
.finish(trailers, scope)
.inspect_err(|_| scope.cancel("response write failure"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct Body(Vec<Vec<u8>>);
impl BodyReader for Body {
fn next_chunk(&mut self, _: &RequestScope) -> io::Result<Option<Vec<u8>>> {
Ok(if self.0.is_empty() {
None
} else {
Some(self.0.remove(0))
})
}
}
#[derive(Default)]
struct Writer {
chunks: Vec<Vec<u8>>,
fail_after: usize,
}
impl ResponseWriter for Writer {
fn write_head(&mut self, _: ResponseHead, _: &RequestScope) -> io::Result<()> {
Ok(())
}
fn write_chunk(&mut self, chunk: &[u8], _: &RequestScope) -> io::Result<()> {
if self.chunks.len() == self.fail_after {
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "peer dropped"));
}
self.chunks.push(chunk.to_vec());
Ok(())
}
fn finish(&mut self, _: &[Header], _: &RequestScope) -> io::Result<()> {
Ok(())
}
}
struct Connection {
head: RequestHead,
body: Body,
writer: Writer,
}
impl RawConnection for Connection {
fn parts(&mut self) -> (&RequestHead, &mut dyn BodyReader, &mut dyn ResponseWriter) {
(&self.head, &mut self.body, &mut self.writer)
}
}
struct Streaming {
observed: Arc<Mutex<Option<Cancellation>>>,
}
impl RawHandler for Streaming {
fn handle(
&self,
_: &RequestHead,
body: &mut dyn BodyReader,
out: &mut dyn ResponseWriter,
scope: &RequestScope,
) -> Result<()> {
*self.observed.lock().unwrap() = Some(scope.cancellation().clone());
while let Some(chunk) = body
.next_chunk(scope)
.map_err(|e| Error::HostError(e.to_string()))?
{
out.write_chunk(&chunk, scope)
.map_err(|e| Error::HostError(e.to_string()))?;
}
Ok(())
}
}
#[test]
fn streaming_handler_is_backpressured_and_cancelled_on_peer_drop() {
let observed = Arc::new(Mutex::new(None));
let server = RawHttpServer::new(
Streaming {
observed: Arc::clone(&observed),
},
BodyLimits {
max_request_bytes: 16,
max_chunk_bytes: 4,
},
TrailersPolicy::Deny,
Duration::from_secs(1),
)
.unwrap();
let mut connection = Connection {
head: RequestHead {
method: "POST".into(),
target: "/mcp".into(),
headers: vec![("X-A".into(), "1".into()), ("X-A".into(), "2".into())],
peer: Some("peer".into()),
local: Some("local".into()),
},
body: Body(vec![b"one".to_vec(), b"two".to_vec()]),
writer: Writer {
fail_after: 1,
..Writer::default()
},
};
assert!(server.serve(&mut connection, &Cancellation::new()).is_err());
assert_eq!(connection.writer.chunks, vec![b"one".to_vec()]);
assert!(observed.lock().unwrap().as_ref().unwrap().is_cancelled());
}
}