Skip to main content

secretspec_ipc/
jsonrpc.rs

1use crate::MAX_REQUEST_ID;
2use crate::error::{Error, ErrorKind, Result, RpcError};
3use serde::de::{DeserializeSeed, Error as _, MapAccess, SeqAccess, Visitor};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::Value;
6use std::collections::HashSet;
7use std::fmt;
8
9/// A positive, session-unique JSON request ID.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct RequestId(u64);
12
13impl RequestId {
14    pub fn new(value: u64) -> Result<Self> {
15        if value == 0 || value > MAX_REQUEST_ID {
16            return Err(Error::Protocol("request ID is outside the version 1 range"));
17        }
18        Ok(Self(value))
19    }
20
21    pub const fn get(self) -> u64 {
22        self.0
23    }
24}
25
26impl Serialize for RequestId {
27    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
28    where
29        S: Serializer,
30    {
31        serializer.serialize_u64(self.0)
32    }
33}
34
35impl<'de> Deserialize<'de> for RequestId {
36    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
37    where
38        D: Deserializer<'de>,
39    {
40        let value = u64::deserialize(deserializer)?;
41        if value == 0 || value > MAX_REQUEST_ID {
42            return Err(D::Error::custom("invalid version 1 request ID"));
43        }
44        Ok(Self(value))
45    }
46}
47
48/// The JSON-RPC version literal. A dedicated type prevents accepting another
49/// string through otherwise valid derived structures.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub struct Version;
52
53impl Serialize for Version {
54    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
55    where
56        S: Serializer,
57    {
58        serializer.serialize_str("2.0")
59    }
60}
61
62impl<'de> Deserialize<'de> for Version {
63    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
64    where
65        D: Deserializer<'de>,
66    {
67        let value = String::deserialize(deserializer)?;
68        if value == "2.0" {
69            Ok(Self)
70        } else {
71            Err(D::Error::custom("jsonrpc must be \"2.0\""))
72        }
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct Meta {
79    /// Mandatory absolute end-to-end deadline. Generic RPC metadata lives
80    /// under this one reserved member instead of expanding the envelope.
81    pub deadline_unix_ms: u64,
82    /// A callback names the still-active request that caused it. Ordinary
83    /// client-to-server requests omit this member.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub parent_request_id: Option<RequestId>,
86}
87
88// Params and results carry secret values (`resolver.set`, `provider.get`), so
89// `Debug` for the JSON-RPC envelopes shows only routing fields.
90impl std::fmt::Debug for Request {
91    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        formatter
93            .debug_struct("Request")
94            .field("id", &self.id)
95            .field("method", &self.method)
96            .field("meta", &self.meta)
97            .field("params", &crate::protocol::Redacted)
98            .finish()
99    }
100}
101
102impl std::fmt::Debug for Notification {
103    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        formatter
105            .debug_struct("Notification")
106            .field("method", &self.method)
107            .field("params", &crate::protocol::Redacted)
108            .finish()
109    }
110}
111
112impl std::fmt::Debug for SuccessResponse {
113    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        formatter
115            .debug_struct("SuccessResponse")
116            .field("id", &self.id)
117            .field("result", &crate::protocol::Redacted)
118            .finish()
119    }
120}
121
122#[derive(Clone, PartialEq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct Request {
125    pub jsonrpc: Version,
126    pub id: RequestId,
127    pub method: String,
128    #[serde(rename = "_meta")]
129    pub meta: Meta,
130    pub params: Value,
131}
132
133impl Request {
134    pub fn new(
135        id: RequestId,
136        method: impl Into<String>,
137        deadline_unix_ms: u64,
138        params: Value,
139    ) -> Result<Self> {
140        let method = method.into();
141        validate_method_and_params(&method, &params)?;
142        Ok(Self {
143            jsonrpc: Version,
144            id,
145            method,
146            meta: Meta {
147                deadline_unix_ms,
148                parent_request_id: None,
149            },
150            params,
151        })
152    }
153
154    pub const fn deadline_unix_ms(&self) -> u64 {
155        self.meta.deadline_unix_ms
156    }
157
158    pub fn with_parent_request_id(mut self, parent_request_id: RequestId) -> Self {
159        self.meta.parent_request_id = Some(parent_request_id);
160        self
161    }
162}
163
164#[derive(Clone, PartialEq, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct Notification {
167    pub jsonrpc: Version,
168    pub method: String,
169    pub params: Value,
170}
171
172impl Notification {
173    pub fn new(method: impl Into<String>, params: Value) -> Result<Self> {
174        let method = method.into();
175        validate_method_and_params(&method, &params)?;
176        Ok(Self {
177            jsonrpc: Version,
178            method,
179            params,
180        })
181    }
182}
183
184#[derive(Clone, PartialEq, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct SuccessResponse {
187    pub jsonrpc: Version,
188    pub id: RequestId,
189    pub result: Value,
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct ErrorResponse {
195    pub jsonrpc: Version,
196    #[serde(deserialize_with = "crate::protocol::deserialize_required_nullable")]
197    pub id: Option<RequestId>,
198    pub error: RpcError,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202#[serde(untagged)]
203pub enum Response {
204    Success(SuccessResponse),
205    Error(ErrorResponse),
206}
207
208impl Response {
209    pub fn success(id: RequestId, result: Value) -> Self {
210        Self::Success(SuccessResponse {
211            jsonrpc: Version,
212            id,
213            result,
214        })
215    }
216
217    pub fn error(id: Option<RequestId>, error: RpcError) -> Self {
218        Self::Error(ErrorResponse {
219            jsonrpc: Version,
220            id,
221            error,
222        })
223    }
224
225    pub const fn id(&self) -> Option<RequestId> {
226        match self {
227            Self::Success(response) => Some(response.id),
228            Self::Error(response) => response.id,
229        }
230    }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize)]
234#[serde(untagged)]
235pub enum Envelope {
236    Request(Request),
237    Notification(Notification),
238    Response(Response),
239}
240
241impl Envelope {
242    /// Parse one strict JSON-RPC object, rejecting duplicate keys, non-objects,
243    /// unknown envelope members, invalid IDs, and excessive nesting.
244    pub fn parse(bytes: &[u8]) -> Result<Self> {
245        Self::parse_classified(bytes).map_err(|(error, _)| error)
246    }
247
248    /// Parse, reporting the error kind the frame should be answered with.
249    ///
250    /// Malformed bytes yield `parse_error`; anything that parsed as JSON but
251    /// broke a rule above it (duplicate keys, nesting depth, unknown members,
252    /// invalid IDs, method and params shape) yields `invalid_request`. Callers
253    /// that must answer with an error kind use this instead of re-parsing the
254    /// frame with a laxer parser to guess which layer failed.
255    pub fn parse_classified(bytes: &[u8]) -> std::result::Result<Self, (Error, ErrorKind)> {
256        let value = parse_strict_value(bytes)?;
257        Self::from_strict_value(value).map_err(|error| (error, ErrorKind::InvalidRequest))
258    }
259
260    fn from_strict_value(value: Value) -> Result<Self> {
261        let object = value
262            .as_object()
263            .ok_or(Error::Protocol("JSON-RPC payload is not an object"))?;
264
265        let envelope = if object.contains_key("method") && object.contains_key("id") {
266            Self::Request(from_value(value)?)
267        } else if object.contains_key("method") {
268            Self::Notification(from_value(value)?)
269        } else if object.contains_key("result") || object.contains_key("error") {
270            let response: Response = from_value(value)?;
271            if let Response::Error(error) = &response {
272                error.error.validate().map_err(Error::Protocol)?;
273            }
274            Self::Response(response)
275        } else {
276            return Err(Error::Protocol("unrecognized JSON-RPC envelope"));
277        };
278
279        match &envelope {
280            Self::Request(request) => {
281                validate_method_and_params(&request.method, &request.params)?;
282            }
283            Self::Notification(notification) => {
284                validate_method_and_params(&notification.method, &notification.params)?;
285            }
286            Self::Response(_) => {}
287        }
288        Ok(envelope)
289    }
290
291    pub fn to_vec(&self) -> Result<Vec<u8>> {
292        serde_json::to_vec(self).map_err(|error| Error::ProtocolOwned(error.to_string()))
293    }
294}
295
296/// Borrows rather than consuming: parsing validates an already-built envelope,
297/// and cloning `params` there duplicated the whole tree (up to a full frame,
298/// including any secret it carries) only to be dropped again.
299fn validate_method_and_params(method: &str, params: &Value) -> Result<()> {
300    if method.is_empty() || method.len() > 256 {
301        return Err(Error::Protocol("method has an invalid byte length"));
302    }
303    if !params.is_object() {
304        return Err(Error::Protocol("params must be an object"));
305    }
306    Ok(())
307}
308
309fn from_value<T: for<'de> Deserialize<'de>>(value: Value) -> Result<T> {
310    serde_json::from_value(value).map_err(|error| Error::ProtocolOwned(error.to_string()))
311}
312
313fn parse_strict_value(bytes: &[u8]) -> std::result::Result<Value, (Error, ErrorKind)> {
314    let mut deserializer = serde_json::Deserializer::from_slice(bytes);
315    let value = StrictValueSeed { depth: 0 }
316        .deserialize(&mut deserializer)
317        .map_err(classify_json_error)?;
318    deserializer.end().map_err(classify_json_error)?;
319    Ok(value)
320}
321
322/// Separates "this was not JSON" from "this was JSON we refuse".
323///
324/// `Syntax`/`Eof` mean the bytes are malformed, which is `parse_error`. `Data`
325/// means the document parsed and one of the strict reader's own rules rejected
326/// it (a duplicate key, or nesting past the limit); the peer sent well-formed
327/// JSON, so that stays `invalid_request`. Reading the category off the existing
328/// failure avoids re-parsing the frame just to tell the two apart.
329fn classify_json_error(error: serde_json::Error) -> (Error, ErrorKind) {
330    let kind = match error.classify() {
331        serde_json::error::Category::Data => ErrorKind::InvalidRequest,
332        _ => ErrorKind::ParseError,
333    };
334    (Error::ProtocolOwned(error.to_string()), kind)
335}
336
337const MAX_NESTING: usize = 64;
338
339struct StrictValueSeed {
340    depth: usize,
341}
342
343impl<'de> DeserializeSeed<'de> for StrictValueSeed {
344    type Value = Value;
345
346    fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
347    where
348        D: Deserializer<'de>,
349    {
350        deserializer.deserialize_any(StrictValueVisitor { depth: self.depth })
351    }
352}
353
354struct StrictValueVisitor {
355    depth: usize,
356}
357
358impl StrictValueVisitor {
359    fn child<E: serde::de::Error>(&self) -> std::result::Result<StrictValueSeed, E> {
360        if self.depth >= MAX_NESTING {
361            Err(E::custom("JSON nesting exceeds 64 containers"))
362        } else {
363            Ok(StrictValueSeed {
364                depth: self.depth + 1,
365            })
366        }
367    }
368}
369
370impl<'de> Visitor<'de> for StrictValueVisitor {
371    type Value = Value;
372
373    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374        formatter.write_str("an RFC 8259 JSON value without duplicate object keys")
375    }
376
377    fn visit_bool<E>(self, value: bool) -> std::result::Result<Self::Value, E> {
378        Ok(Value::Bool(value))
379    }
380
381    fn visit_i64<E>(self, value: i64) -> std::result::Result<Self::Value, E> {
382        Ok(Value::Number(value.into()))
383    }
384
385    fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E> {
386        Ok(Value::Number(value.into()))
387    }
388
389    fn visit_f64<E>(self, value: f64) -> std::result::Result<Self::Value, E>
390    where
391        E: serde::de::Error,
392    {
393        serde_json::Number::from_f64(value)
394            .map(Value::Number)
395            .ok_or_else(|| E::custom("non-finite JSON number"))
396    }
397
398    fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E> {
399        Ok(Value::String(value.to_string()))
400    }
401
402    fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E> {
403        Ok(Value::String(value))
404    }
405
406    fn visit_none<E>(self) -> std::result::Result<Self::Value, E> {
407        Ok(Value::Null)
408    }
409
410    fn visit_unit<E>(self) -> std::result::Result<Self::Value, E> {
411        Ok(Value::Null)
412    }
413
414    fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
415    where
416        D: Deserializer<'de>,
417    {
418        StrictValueSeed { depth: self.depth }.deserialize(deserializer)
419    }
420
421    fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
422    where
423        A: SeqAccess<'de>,
424    {
425        let mut values = Vec::new();
426        while let Some(value) = sequence.next_element_seed(self.child()?)? {
427            values.push(value);
428        }
429        Ok(Value::Array(values))
430    }
431
432    fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
433    where
434        A: MapAccess<'de>,
435    {
436        let mut values = serde_json::Map::new();
437        let mut keys = HashSet::new();
438        while let Some(key) = map.next_key::<String>()? {
439            if !keys.insert(key.clone()) {
440                return Err(A::Error::custom("duplicate JSON object key"));
441            }
442            let value = map.next_value_seed(self.child()?)?;
443            values.insert(key, value);
444        }
445        Ok(Value::Object(values))
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn rejection_reports_the_layer_that_refused_the_frame() {
455        // Only malformed bytes are `parse_error`. Everything that parsed as
456        // JSON is `invalid_request`, however far up it was rejected. Pinned
457        // because this is peer-visible and the classification is now read off
458        // the parse failure rather than recovered by re-parsing the frame.
459        for payload in [
460            br#"{"jsonrpc":"2.0",}"#.as_slice(),
461            br#"{"jsonrpc":"2.0""#.as_slice(),
462            b"\xff\xfe".as_slice(),
463        ] {
464            let (_, kind) = Envelope::parse_classified(payload).unwrap_err();
465            assert_eq!(kind, ErrorKind::ParseError, "{payload:?}");
466        }
467
468        let deep = format!("{}{}", "[".repeat(80), "]".repeat(80));
469        for payload in [
470            // Well-formed JSON the strict reader refuses on its own rules.
471            br#"{"jsonrpc":"2.0","jsonrpc":"2.0"}"#.as_slice(),
472            deep.as_bytes(),
473            // A batch array parses as JSON; JSON-RPC 2.0 answers an array with
474            // invalid_request rather than parse_error.
475            br#"[]"#.as_slice(),
476            // Envelope-layer rejections.
477            br#"{"jsonrpc":"2.0","id":0,"method":"x","_meta":{"deadline_unix_ms":1},"params":{}}"#
478                .as_slice(),
479            br#"{"jsonrpc":"2.0","id":1,"method":"","_meta":{"deadline_unix_ms":1},"params":{}}"#
480                .as_slice(),
481            br#"{"jsonrpc":"2.0","id":1,"method":"x","_meta":{"deadline_unix_ms":1},"params":[]}"#
482                .as_slice(),
483            br#"{"jsonrpc":"2.0"}"#.as_slice(),
484        ] {
485            let (_, kind) = Envelope::parse_classified(payload).unwrap_err();
486            assert_eq!(kind, ErrorKind::InvalidRequest, "{payload:?}");
487        }
488    }
489
490    #[test]
491    fn debug_hides_params_and_results() {
492        for payload in [
493            br#"{"jsonrpc":"2.0","id":1,"method":"resolver.set","_meta":{"deadline_unix_ms":1},"params":{"value":"hunter2"}}"#
494                .as_slice(),
495            br#"{"jsonrpc":"2.0","id":1,"result":{"value":"hunter2"}}"#.as_slice(),
496            br#"{"jsonrpc":"2.0","method":"x.note","params":{"value":"hunter2"}}"#.as_slice(),
497        ] {
498            let debug = format!("{:?}", Envelope::parse(payload).unwrap());
499            assert!(!debug.contains("hunter2"), "{debug}");
500        }
501    }
502
503    #[test]
504    fn rejects_duplicate_keys_and_batches() {
505        assert!(
506            Envelope::parse(
507                br#"{"jsonrpc":"2.0","jsonrpc":"2.0","id":1,"method":"x","_meta":{"deadline_unix_ms":1},"params":{}}"#
508            )
509            .is_err()
510        );
511        assert!(Envelope::parse(br#"[]"#).is_err());
512    }
513
514    #[test]
515    fn request_ids_are_bounded_integers() {
516        for id in ["0", "-1", "1.5", "\"1\"", "9007199254740992"] {
517            let input = format!(
518                r#"{{"jsonrpc":"2.0","id":{id},"method":"x","_meta":{{"deadline_unix_ms":1}},"params":{{}}}}"#
519            );
520            assert!(Envelope::parse(input.as_bytes()).is_err(), "accepted {id}");
521        }
522    }
523
524    #[test]
525    fn rejects_unknown_top_level_members() {
526        assert!(
527            Envelope::parse(
528                br#"{"jsonrpc":"2.0","id":1,"method":"x","_meta":{"deadline_unix_ms":1},"params":{},"extra":true}"#
529            )
530                .is_err()
531        );
532        assert!(Envelope::parse(br#"{"jsonrpc":"2.0","id":1,"result":{},"extra":true}"#).is_err());
533        assert!(
534            Envelope::parse(
535                br#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":-32603,"message":"internal error","data":{"kind":"internal","retryable":false}}}"#
536            )
537            .is_err()
538        );
539    }
540
541    #[test]
542    fn notifications_are_closed_envelopes_with_open_methods() {
543        for payload in [
544            br#"{"jsonrpc":"2.0","method":"future.notice","params":{}}"#.as_slice(),
545            br#"{"jsonrpc":"2.0","method":"rpc.cancel","params":{"id":"bad"}}"#.as_slice(),
546        ] {
547            assert!(matches!(
548                Envelope::parse(payload),
549                Ok(Envelope::Notification(_))
550            ));
551        }
552        assert!(
553            Envelope::parse(
554                br#"{"jsonrpc":"2.0","method":"future.notice","params":{},"extra":true}"#
555            )
556            .is_err()
557        );
558    }
559
560    #[test]
561    fn request_deadline_is_required_and_round_trips() {
562        assert!(Envelope::parse(br#"{"jsonrpc":"2.0","id":1,"method":"x","params":{}}"#).is_err());
563        let parsed = Envelope::parse(
564            br#"{"jsonrpc":"2.0","id":1,"method":"x","_meta":{"deadline_unix_ms":42},"params":{}}"#,
565        )
566        .unwrap();
567        let Envelope::Request(request) = parsed else {
568            panic!("expected request")
569        };
570        assert_eq!(request.deadline_unix_ms(), 42);
571    }
572
573    #[test]
574    fn error_response_requires_an_explicit_nullable_id() {
575        assert!(
576            Envelope::parse(
577                br#"{"jsonrpc":"2.0","error":{"code":-32700,"message":"parse error","data":{"kind":"parse_error","retryable":false}}}"#
578            )
579            .is_err()
580        );
581        assert!(
582            Envelope::parse(
583                br#"{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"parse error","data":{"kind":"parse_error","retryable":false}}}"#
584            )
585            .is_ok()
586        );
587    }
588}