Skip to main content

confium_daemon/
server.rs

1//! JSON-RPC server loop.
2//!
3//! Accepts connections on a [`tokio::net::TcpListener`] or Unix socket
4//! and serves each with a task that reads length-prefixed JSON-RPC
5//! messages, dispatches them, and writes responses.
6//!
7//! Framing: each message is a 4-byte big-endian length prefix followed
8//! by that many bytes of UTF-8 JSON. This is the same framing used by
9//! LSP / DAP and most production JSON-RPC daemons; it avoids the
10//! ambiguity of newline-delimited JSON when payloads contain embedded
11//! newlines.
12//!
13//! Threading: the Confium engine holds `Rc<dyn Any>` plugin interfaces,
14//! which makes it `!Send`. The server therefore runs on a single
15//! [`tokio::task::LocalSet`] and keeps the engine behind
16//! `Rc<RefCell<Confium>>`. Connections are driven concurrently by the
17//! LocalSet's cooperative scheduler; Confium access is serialized by
18//! the `RefCell` borrow. This is the right shape for a skeleton — the
19//! engine is single-threaded by construction (the C FFI assumes it),
20//! and moving to a multi-threaded actor model is a later optimization.
21
22use std::cell::RefCell;
23use std::rc::Rc;
24
25use confium_core::Confium;
26use serde_json::Value;
27use tokio::io::{AsyncReadExt, AsyncWriteExt};
28use tokio::net::TcpListener;
29use tokio::task::LocalSet;
30use tokio_util::sync::CancellationToken;
31
32use crate::dispatch::Dispatch;
33use crate::error::{self, DaemonError, RpcError};
34use crate::protocol::{RpcRequest, RpcResponse};
35
36/// Type alias for the shared engine handle. `Rc<RefCell<...>>` because
37/// `Confium` is `!Send` (plugin interfaces are `Rc<dyn Any>`).
38pub type SharedConfium = Rc<RefCell<Confium>>;
39
40/// The daemon's shared state: the engine, the dispatch table, and a
41/// shutdown signal.
42pub struct Server {
43    /// The single Confium instance owned by the daemon. All method
44    /// handlers dispatch against this.
45    pub cfm: SharedConfium,
46
47    /// Method-name → handler table.
48    pub dispatch: Dispatch,
49
50    /// Cancellation token: set when `shutdown` is called or the process
51    /// receives a signal. Stops the accept loop and drains.
52    pub cancel: CancellationToken,
53}
54
55impl Server {
56    /// Construct a server with a fresh Confium (audit logger resolved
57    /// from the environment) and the default dispatch table.
58    pub fn new() -> Self {
59        Server {
60            cfm: Rc::new(RefCell::new(Confium::new())),
61            dispatch: Dispatch::new(),
62            cancel: CancellationToken::new(),
63        }
64    }
65
66    /// Construct a server with an explicit Confium (used by tests that
67    /// want the audit logger disabled).
68    pub fn with_confium(cfm: Confium) -> Self {
69        Server {
70            cfm: Rc::new(RefCell::new(cfm)),
71            dispatch: Dispatch::new(),
72            cancel: CancellationToken::new(),
73        }
74    }
75
76    /// Run the accept loop on a TCP listener until shutdown. Must be
77    /// called from within a [`LocalSet`] — see [`Server::run_tcp`].
78    pub async fn serve_tcp(self: Rc<Self>, listener: TcpListener) -> error::Result<()> {
79        let shutdown = self.cancel.clone();
80        loop {
81            tokio::select! {
82                biased;
83                _ = shutdown.cancelled() => break,
84                accept = listener.accept() => {
85                    let (stream, _peer) = accept?;
86                    let server = Rc::clone(&self);
87                    // spawn_local so the !Send Confium stays on this
88                    // thread's LocalSet.
89                    tokio::task::spawn_local(async move {
90                        let _ = server.handle_tcp(stream).await;
91                    });
92                }
93            }
94        }
95        Ok(())
96    }
97
98    /// Run the accept loop on a Unix socket listener until shutdown.
99    #[cfg(unix)]
100    pub async fn serve_unix(
101        self: Rc<Self>,
102        listener: tokio::net::UnixListener,
103    ) -> error::Result<()> {
104        let shutdown = self.cancel.clone();
105        loop {
106            tokio::select! {
107                biased;
108                _ = shutdown.cancelled() => break,
109                accept = listener.accept() => {
110                    let (stream, _peer) = accept?;
111                    let server = Rc::clone(&self);
112                    tokio::task::spawn_local(async move {
113                        let _ = server.handle_unix(stream).await;
114                    });
115                }
116            }
117        }
118        Ok(())
119    }
120
121    /// Drive a TCP listener inside a [`LocalSet`] until shutdown. This
122    /// is the convenience entry point for `main` and tests: it creates
123    /// the LocalSet, enters it, and runs the accept loop.
124    pub async fn run_tcp(self: Rc<Self>, listener: TcpListener) -> error::Result<()> {
125        let local = LocalSet::new();
126        local.run_until(self.serve_tcp(listener)).await
127    }
128
129    /// Drive a Unix listener inside a [`LocalSet`] until shutdown.
130    #[cfg(unix)]
131    pub async fn run_unix(self: Rc<Self>, listener: tokio::net::UnixListener) -> error::Result<()> {
132        let local = LocalSet::new();
133        local.run_until(self.serve_unix(listener)).await
134    }
135
136    /// Handle a single TCP connection: read length-prefixed requests,
137    /// dispatch, write responses. Returns when the peer closes the
138    /// connection or an unrecoverable I/O error occurs.
139    async fn handle_tcp(self: Rc<Self>, stream: tokio::net::TcpStream) -> error::Result<()> {
140        let (mut read, mut write) = tokio::io::split(stream);
141        Self::drive_connection(&self, &mut read, &mut write).await
142    }
143
144    /// Handle a single Unix socket connection.
145    #[cfg(unix)]
146    async fn handle_unix(self: Rc<Self>, stream: tokio::net::UnixStream) -> error::Result<()> {
147        let (mut read, mut write) = tokio::io::split(stream);
148        Self::drive_connection(&self, &mut read, &mut write).await
149    }
150
151    /// Shared connection loop: read a message, dispatch, write reply.
152    /// Generic over the split read/write halves.
153    async fn drive_connection<R, W>(
154        self: &Rc<Self>,
155        read: &mut R,
156        write: &mut W,
157    ) -> error::Result<()>
158    where
159        R: AsyncReadExt + Unpin,
160        W: AsyncWriteExt + Unpin,
161    {
162        loop {
163            let msg = match read_length_prefixed(read).await? {
164                Some(m) => m,
165                None => return Ok(()), // EOF
166            };
167            let response = self.process(&msg).await;
168            if let Some(resp) = response {
169                let bytes = serde_json::to_vec(&resp)?;
170                write_length_prefixed(write, &bytes).await?;
171            }
172            if self.cancel.is_cancelled() {
173                break;
174            }
175        }
176        Ok(())
177    }
178
179    /// Parse a raw JSON message, dispatch, and produce the response
180    /// (or `None` for notifications).
181    async fn process(self: &Rc<Self>, raw: &[u8]) -> Option<RpcResponse> {
182        let req: RpcRequest = match serde_json::from_slice(raw) {
183            Ok(r) => r,
184            Err(e) => {
185                return Some(RpcResponse::err(
186                    Value::Null,
187                    RpcError::InvalidParams {
188                        detail: format!("parse error: {e}"),
189                    },
190                ));
191            }
192        };
193
194        if !req.version_ok() {
195            return Some(RpcResponse::err(
196                req.id.unwrap_or(Value::Null),
197                RpcError::InvalidParams {
198                    detail: "jsonrpc must be \"2.0\"".to_string(),
199                },
200            ));
201        }
202
203        // Special-case `shutdown`: reply then cancel the accept loop.
204        let is_shutdown = req.method == "shutdown";
205
206        let handler = match self.dispatch.get(&req.method) {
207            Some(h) => h,
208            None => {
209                return if req.is_notification() {
210                    None
211                } else {
212                    Some(RpcResponse::err(
213                        req.id.unwrap_or(Value::Null),
214                        RpcError::MethodNotFound {
215                            method: req.method.clone(),
216                        },
217                    ))
218                };
219            }
220        };
221
222        let result = handler(Rc::clone(&self.cfm), req.params.clone()).await;
223
224        if is_shutdown {
225            self.cancel.cancel();
226        }
227
228        if req.is_notification() {
229            return None;
230        }
231
232        let id = req.id.unwrap_or(Value::Null);
233        Some(match result {
234            Ok(value) => RpcResponse::ok(id, value),
235            Err(err) => RpcResponse::err(id, err),
236        })
237    }
238}
239
240impl Default for Server {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246// --- length-prefixed framing -------------------------------------------
247
248/// Read one length-prefixed message. Returns `None` on clean EOF.
249///
250/// Reads 4 bytes (big-endian u32 length), then that many bytes of
251/// payload.
252async fn read_length_prefixed<R: AsyncReadExt + Unpin>(
253    reader: &mut R,
254) -> error::Result<Option<Vec<u8>>> {
255    let mut len_buf = [0u8; 4];
256    // A clean EOF before any bytes means the peer closed; a partial
257    // read is an error.
258    match reader.read_exact(&mut len_buf).await {
259        Ok(_) => {}
260        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
261        Err(e) => return Err(DaemonError::Io { source: e }),
262    }
263    let len = u32::from_be_bytes(len_buf) as usize;
264    // Cap the message size to avoid a hostile peer forcing a huge
265    // allocation. 16 MiB is well above any legitimate JSON-RPC payload.
266    const MAX_MESSAGE: usize = 16 * 1024 * 1024;
267    if len > MAX_MESSAGE {
268        return Err(DaemonError::Io {
269            source: std::io::Error::new(
270                std::io::ErrorKind::InvalidData,
271                format!("message length {len} exceeds {MAX_MESSAGE}"),
272            ),
273        });
274    }
275    let mut buf = vec![0u8; len];
276    reader.read_exact(&mut buf).await?;
277    Ok(Some(buf))
278}
279
280/// Write a length-prefixed message.
281async fn write_length_prefixed<W: AsyncWriteExt + Unpin>(
282    writer: &mut W,
283    payload: &[u8],
284) -> error::Result<()> {
285    let len = payload.len() as u32;
286    writer.write_all(&len.to_be_bytes()).await?;
287    writer.write_all(payload).await?;
288    writer.flush().await?;
289    Ok(())
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use confium_core::audit::AuditLogger;
296
297    fn server() -> Server {
298        Server::with_confium(Confium::new_with_audit(AuditLogger::disabled()))
299    }
300
301    #[tokio::test]
302    async fn process_unknown_method_returns_method_not_found() {
303        let local = LocalSet::new();
304        local
305            .run_until(async {
306                let s = Rc::new(server());
307                let raw = br#"{"jsonrpc":"2.0","id":1,"method":"does_not_exist","params":{}}"#;
308                let resp = s.process(raw).await.unwrap();
309                let serialized = serde_json::to_value(&resp).unwrap();
310                assert_eq!(
311                    serialized["error"]["code"],
312                    RpcError::MethodNotFound { method: "".into() }.code()
313                );
314            })
315            .await;
316    }
317
318    #[tokio::test]
319    async fn process_version_returns_pkg_version() {
320        let local = LocalSet::new();
321        local
322            .run_until(async {
323                let s = Rc::new(server());
324                let raw = br#"{"jsonrpc":"2.0","id":1,"method":"version","params":{}}"#;
325                let resp = s.process(raw).await.unwrap();
326                let serialized = serde_json::to_value(&resp).unwrap();
327                assert_eq!(serialized["result"]["version"], env!("CARGO_PKG_VERSION"));
328            })
329            .await;
330    }
331
332    #[tokio::test]
333    async fn process_bad_json_returns_parse_error() {
334        let local = LocalSet::new();
335        local
336            .run_until(async {
337                let s = Rc::new(server());
338                let resp = s.process(b"not json").await.unwrap();
339                let serialized = serde_json::to_value(&resp).unwrap();
340                assert!(
341                    serialized["error"]["message"]
342                        .as_str()
343                        .unwrap()
344                        .contains("parse error")
345                );
346            })
347            .await;
348    }
349
350    #[tokio::test]
351    async fn process_notification_returns_none() {
352        let local = LocalSet::new();
353        local
354            .run_until(async {
355                let s = Rc::new(server());
356                // No "id" → notification. The server must not reply.
357                let raw = br#"{"jsonrpc":"2.0","method":"version","params":{}}"#;
358                let resp = s.process(raw).await;
359                assert!(resp.is_none());
360            })
361            .await;
362    }
363
364    #[tokio::test]
365    async fn shutdown_triggers_cancellation() {
366        let local = LocalSet::new();
367        local
368            .run_until(async {
369                let s = Rc::new(server());
370                let raw = br#"{"jsonrpc":"2.0","id":1,"method":"shutdown","params":{}}"#;
371                let _ = s.process(raw).await;
372                assert!(s.cancel.is_cancelled());
373            })
374            .await;
375    }
376
377    #[tokio::test]
378    async fn length_prefixed_roundtrip() {
379        // Write then read a length-prefixed message through an
380        // in-memory pipe.
381        let payload = br#"{"jsonrpc":"2.0","id":1,"method":"version"}"#;
382        let mut buf = Vec::new();
383        write_length_prefixed(&mut buf, payload).await.unwrap();
384
385        let mut cursor = std::io::Cursor::new(buf);
386        let msg = read_length_prefixed(&mut cursor).await.unwrap().unwrap();
387        assert_eq!(msg, payload);
388    }
389}