use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncReadExt as _, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
use super::{JSONRPC_VERSION, RELAY_METHOD, RelayDelivery, RelayRequest, RelayResponse};
use crate::uds::{MAX_FRAME_BYTES, ensure_peer_is_self};
pub const CODE_PARSE_ERROR: i64 = -32700;
pub const CODE_INVALID_REQUEST: i64 = -32600;
pub const CODE_METHOD_NOT_FOUND: i64 = -32601;
pub const CODE_INVALID_PARAMS: i64 = -32602;
pub const CODE_NOT_DURABLE: i64 = -32000;
pub const LISTENER_SHUTDOWN_FLUSH: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SinkRejection {
pub code: i64,
pub message: String,
}
impl SinkRejection {
pub fn not_durable(message: impl Into<String>) -> Self {
Self {
code: CODE_NOT_DURABLE,
message: message.into(),
}
}
}
pub trait DeliverySink: Send + Sync + 'static {
fn take_ownership(&self, delivery: &RelayDelivery) -> Result<(), SinkRejection>;
}
impl DeliverySink for super::Inbox {
fn take_ownership(&self, delivery: &RelayDelivery) -> Result<(), SinkRejection> {
match super::Inbox::take_ownership(self, delivery) {
Ok(ownership) => {
tracing::info!(
delivery_id = %delivery.delivery_id,
source = %delivery.source,
event = %delivery.event,
already_owned = ownership.already_owned,
path = %ownership.path.display(),
"webhook delivery is durably held; acknowledging"
);
Ok(())
}
Err(e) => {
tracing::error!(
delivery_id = %delivery.delivery_id,
error = %e,
"could not take durable ownership; refusing so the sender keeps its copy"
);
Err(SinkRejection::not_durable(format!("{e}")))
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ServeOptions {
pub read_timeout: Duration,
pub max_frame_bytes: u64,
}
impl Default for ServeOptions {
fn default() -> Self {
Self {
read_timeout: Duration::from_secs(1),
max_frame_bytes: MAX_FRAME_BYTES,
}
}
}
pub fn dispatch_frame(frame: &[u8], sink: &dyn DeliverySink) -> RelayResponse {
let request: RelayRequest = match serde_json::from_slice(frame) {
Ok(r) => r,
Err(e) => {
return RelayResponse::refuse(
CODE_PARSE_ERROR,
format!("unparseable relay frame: {e}"),
);
}
};
if request.jsonrpc != JSONRPC_VERSION {
return RelayResponse::refuse(
CODE_INVALID_REQUEST,
format!(
"unsupported jsonrpc version {:?}; this listener speaks {JSONRPC_VERSION}",
request.jsonrpc
),
);
}
if request.method != RELAY_METHOD {
return RelayResponse::refuse(
CODE_METHOD_NOT_FOUND,
format!(
"unknown method {:?}; this listener serves only {RELAY_METHOD}",
request.method
),
);
}
if request.id != request.params.delivery_id {
return RelayResponse::refuse(
CODE_INVALID_PARAMS,
format!(
"request id {:?} does not match params.delivery_id {:?}",
request.id, request.params.delivery_id
),
);
}
if !request.params.provenance.verified {
return RelayResponse::refuse(
CODE_INVALID_PARAMS,
"frame carries provenance.verified = false; the sender must verify \
before relaying (ADR-0034 Β§3)"
.to_string(),
);
}
match sink.take_ownership(&request.params) {
Ok(()) => RelayResponse::ack(),
Err(rejection) => RelayResponse::refuse(rejection.code, rejection.message),
}
}
pub async fn handle_connection(
mut stream: UnixStream,
sink: Arc<dyn DeliverySink>,
options: ServeOptions,
) -> std::io::Result<Served> {
ensure_peer_is_self(&stream).map_err(std::io::Error::other)?;
let mut frame: Vec<u8> = Vec::new();
{
let mut reader = BufReader::new((&mut stream).take(options.max_frame_bytes));
let read = reader.read_until(b'\n', &mut frame).await?;
if read == 0 && frame.is_empty() {
return Ok(Served::LivenessProbe);
}
if !frame.ends_with(b"\n") && frame.len() as u64 >= options.max_frame_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"request frame exceeded {} bytes without a newline",
options.max_frame_bytes
),
));
}
}
let response = tokio::task::spawn_blocking(move || dispatch_frame(&frame, sink.as_ref()))
.await
.map_err(|join| std::io::Error::other(format!("dispatch task did not complete: {join}")))?;
let acked = response.is_ack();
let mut bytes = serde_json::to_vec(&response).map_err(std::io::Error::other)?;
bytes.push(b'\n');
stream.write_all(&bytes).await?;
stream.flush().await?;
Ok(Served::Answered { acked })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Served {
Answered {
acked: bool,
},
LivenessProbe,
}
pub async fn serve_until(
listener: &UnixListener,
sink: Arc<dyn DeliverySink>,
options: ServeOptions,
shutdown: impl std::future::Future<Output = ()> + Send,
) {
tokio::pin!(shutdown);
loop {
let accepted = tokio::select! {
biased;
() = &mut shutdown => return,
accepted = listener.accept() => accepted,
};
let stream = match accepted {
Ok((stream, _)) => stream,
Err(e) => {
tracing::warn!(error = %e, "webhook listener accept failed");
continue;
}
};
let sink = Arc::clone(&sink);
tokio::spawn(async move {
match tokio::time::timeout(
options.read_timeout,
handle_connection(stream, sink, options),
)
.await
{
Ok(Ok(Served::Answered { .. })) => {}
Ok(Ok(Served::LivenessProbe)) => {
tracing::debug!("liveness probe connected and closed without a frame");
}
Ok(Err(e)) => {
tracing::warn!(
error = %e,
"webhook delivery connection failed; not acknowledged"
);
}
Err(_) => {
tracing::warn!(
timeout = ?options.read_timeout,
"webhook delivery connection timed out; not acknowledged"
);
}
}
});
}
}