Skip to main content

dig_peer/
rpc.rs

1//! The peer-stream RPC framing — how a typed JSON-RPC call rides one mux stream.
2//!
3//! `dig-rpc` is an HTTP JSON-RPC *server*; the peer surface instead carries JSON-RPC over `dig-nat`'s
4//! multiplexed mTLS streams. dig-peer defines that on-stream framing here and is the CLIENT that
5//! speaks it. Each call opens ONE fresh logical stream, writes a single length-prefixed request body,
6//! reads a single length-prefixed response body, and lets the stream close — a clean request/response
7//! per stream, with concurrency provided by the mux (open many streams).
8//!
9//! ## Framing (normative)
10//!
11//! A body is a `u32` big-endian length prefix followed by that many bytes — the SAME uniform framing
12//! `dig-nat`'s control messages use, so the two never disagree. For an **unsealed** (public-read)
13//! call the body is the JSON of a `JsonRpcRequest`/`JsonRpcResponse`. For a **directed** (sealed)
14//! call the body is the byte-serialized sealed [`dig_message`] envelope wrapping that JSON (§5.4).
15//! The [`MAX_BODY`] bound guards against a malicious length prefix forcing a huge allocation.
16
17use serde::de::DeserializeOwned;
18use serde::Serialize;
19use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
20
21use crate::error::{DigPeerError, Result};
22use dig_nat::SafeText;
23
24/// Maximum length-prefixed body dig-peer will read — guards against a hostile length prefix. Matches
25/// dig-nat's control-frame bound (64 KiB) for small control/RPC messages.
26pub const MAX_BODY: usize = 64 * 1024;
27
28/// Write a length-prefixed body (`u32` big-endian length + bytes) to `w`.
29pub async fn write_framed<W: AsyncWrite + Unpin>(w: &mut W, body: &[u8]) -> Result<()> {
30    if body.len() > MAX_BODY {
31        // Our own sentence with a LENGTH interpolated. `from_untrusted` is still the honest door —
32        // the length is ours here, but a decimal integer cannot carry a control character either
33        // way, so naming the provenance costs nothing and keeps one rule for the whole file.
34        return Err(DigPeerError::Codec(SafeText::from_untrusted(format!(
35            "outbound body {} exceeds the {MAX_BODY}-byte bound",
36            body.len()
37        ))));
38    }
39    w.write_all(&(body.len() as u32).to_be_bytes()).await?;
40    w.write_all(body).await?;
41    w.flush().await?;
42    Ok(())
43}
44
45/// Read one length-prefixed body (`u32` big-endian length + bytes) from `r`, bounded by [`MAX_BODY`].
46pub async fn read_framed<R: AsyncRead + Unpin>(r: &mut R) -> Result<Vec<u8>> {
47    let mut len_buf = [0u8; 4];
48    r.read_exact(&mut len_buf).await?;
49    let len = u32::from_be_bytes(len_buf) as usize;
50    if len > MAX_BODY {
51        // `len` is PEER-CHOSEN — a declared length from the wire. It is an integer, so it cannot
52        // inject; it goes through the untrusted door anyway so the provenance is visible here.
53        return Err(DigPeerError::Codec(SafeText::from_untrusted(format!(
54            "inbound body length {len} exceeds the {MAX_BODY}-byte bound"
55        ))));
56    }
57    let mut body = vec![0u8; len];
58    r.read_exact(&mut body).await?;
59    Ok(body)
60}
61
62/// Serialize a value to a JSON body for the wire.
63pub fn to_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
64    serde_json::to_vec(value).map_err(|e| DigPeerError::codec_from_json(&e))
65}
66
67/// Deserialize a JSON body from the wire into a typed value.
68pub fn from_json<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
69    serde_json::from_slice(bytes).map_err(|e| DigPeerError::codec_from_json(&e))
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    /// **Proves:** a body written by [`write_framed`] is read back byte-identically by
77    /// [`read_framed`] — the length-prefixed framing is self-consistent.
78    #[tokio::test]
79    async fn framed_body_round_trips() {
80        let body = b"{\"jsonrpc\":\"2.0\"}".to_vec();
81        let mut buf = Vec::new();
82        write_framed(&mut buf, &body).await.expect("write");
83        let mut cursor = std::io::Cursor::new(buf);
84        let read = read_framed(&mut cursor).await.expect("read");
85        assert_eq!(read, body);
86    }
87
88    /// **Proves:** an empty body frames + unframes cleanly (a zero-length prefix is valid).
89    #[tokio::test]
90    async fn empty_body_round_trips() {
91        let mut buf = Vec::new();
92        write_framed(&mut buf, &[]).await.expect("write");
93        let mut cursor = std::io::Cursor::new(buf);
94        assert!(read_framed(&mut cursor).await.expect("read").is_empty());
95    }
96
97    /// **Proves:** writing a body over the [`MAX_BODY`] bound is refused, guarding against a caller
98    /// serializing an oversized payload.
99    #[tokio::test]
100    async fn oversized_write_is_refused() {
101        let big = vec![0u8; MAX_BODY + 1];
102        let mut buf = Vec::new();
103        let result = write_framed(&mut buf, &big).await;
104        assert!(matches!(result, Err(DigPeerError::Codec(_))));
105    }
106
107    /// **Proves:** a length prefix over [`MAX_BODY`] is rejected before allocating, guarding against a
108    /// hostile prefix forcing a huge allocation.
109    #[tokio::test]
110    async fn oversized_length_prefix_is_rejected() {
111        let mut framed = ((MAX_BODY + 1) as u32).to_be_bytes().to_vec();
112        framed.extend_from_slice(&[0u8; 8]);
113        let mut cursor = std::io::Cursor::new(framed);
114        assert!(matches!(
115            read_framed(&mut cursor).await,
116            Err(DigPeerError::Codec(_))
117        ));
118    }
119
120    /// **Proves:** [`to_json`]/[`from_json`] round-trip a value; a malformed body is a `Codec` error.
121    #[test]
122    fn json_round_trips_and_rejects_garbage() {
123        let value = serde_json::json!({"a": 1});
124        let bytes = to_json(&value).expect("to_json");
125        let back: serde_json::Value = from_json(&bytes).expect("from_json");
126        assert_eq!(value, back);
127        assert!(matches!(
128            from_json::<serde_json::Value>(b"not json"),
129            Err(DigPeerError::Codec(_))
130        ));
131    }
132}