Skip to main content

nomoreide_remote_protocol/
envelope.rs

1//! The frame every relay message travels in, and the order it is checked in.
2//!
3//! **The envelope is invariant.** `v`, `id`, `type`, `deviceId`, `sentAt`,
4//! `replyTo` and `payload` are fixed for the life of the protocol; `v` versions
5//! the *payload union*, not the frame. That is what lets two peers with no
6//! version in common still exchange a hello, a rejection and a heartbeat rather
7//! than staring at each other — see [`super::version`].
8//!
9//! Validation order is deliberate, and it is cheapest-first only up to a point:
10//!
11//! 1. the byte length, before anything is parsed;
12//! 2. the envelope's own shape;
13//! 3. the protocol version;
14//! 4. whether the `type` is a name this direction accepts;
15//! 5. the payload;
16//! 6. the `replyTo` rule for that type;
17//! 7. the `sentAt` window.
18//!
19//! Staleness is last on purpose. It is the one condition that goes away by
20//! itself, and reporting it ahead of a wrong version or an unknown command
21//! would tell a peer with a permanent bug that it had a transient one.
22
23use super::device_bound::DeviceBound;
24use super::errors::{ErrorCode, ProtocolError};
25use super::limits;
26use super::platform_bound::PlatformBound;
27use super::version::{PROTOCOL_VERSION, SUPPORTED_VERSIONS};
28use chrono::{DateTime, Duration as ChronoDuration, Utc};
29use serde::{Deserialize, Serialize};
30
31/// The most bytes an `id` or `deviceId` may be.
32///
33/// Both become map keys and log fields, and an identifier is not a place to put
34/// data. A UUID is 36 bytes; this leaves room for a prefixed or ULID form
35/// without leaving room for a payload.
36pub const MAX_IDENTIFIER_BYTES: usize = 128;
37
38/// One decoded frame: the invariant header, and the typed body.
39#[derive(Debug, Clone, PartialEq)]
40pub struct Envelope<T> {
41    pub v: u32,
42    /// The request id. Doubles as the idempotency key — see
43    /// [`super::idempotency`].
44    pub id: String,
45    pub device_id: String,
46    pub sent_at: DateTime<Utc>,
47    /// The `id` of the request this frame answers, or `None` for an unsolicited
48    /// one.
49    pub reply_to: Option<String>,
50    pub body: T,
51}
52
53/// The envelope as it appears on the wire, before the body is understood.
54///
55/// `deny_unknown_fields` here is the outermost strictness in the protocol: a
56/// field nobody defined cannot ride along, whatever it is called.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase", deny_unknown_fields)]
59struct RawEnvelope {
60    v: u32,
61    id: String,
62    #[serde(rename = "type")]
63    kind: String,
64    device_id: String,
65    sent_at: DateTime<Utc>,
66    #[serde(default)]
67    reply_to: Option<String>,
68    payload: serde_json::Value,
69}
70
71impl<T> Envelope<T> {
72    /// Build a frame to send. `sent_at` is taken from the caller rather than
73    /// the clock so that tests and the golden fixtures are not time-dependent.
74    /// A frame stamped with the newest version this build speaks.
75    ///
76    /// Right for a peer known to be current, and for tests. A connection that
77    /// has negotiated down must use [`Self::at_version`] instead.
78    pub fn new(
79        id: impl Into<String>,
80        device_id: impl Into<String>,
81        sent_at: DateTime<Utc>,
82        body: T,
83    ) -> Self {
84        Self::at_version(PROTOCOL_VERSION, id, device_id, sent_at, body)
85    }
86
87    /// A frame stamped with a specific version.
88    ///
89    /// **Why this is not just `new`.** Once two versions exist, the version a
90    /// session speaks is the one it *negotiated*, not the one this build
91    /// prefers. A v2 daemon that stamped 2 on every frame after agreeing to
92    /// speak 1 would have every frame refused by the peer that asked it to
93    /// downgrade — which is the entire population of already-deployed peers.
94    pub fn at_version(
95        version: u32,
96        id: impl Into<String>,
97        device_id: impl Into<String>,
98        sent_at: DateTime<Utc>,
99        body: T,
100    ) -> Self {
101        Self {
102            v: version,
103            id: id.into(),
104            device_id: device_id.into(),
105            sent_at,
106            reply_to: None,
107            body,
108        }
109    }
110
111    pub fn in_reply_to(mut self, request_id: impl Into<String>) -> Self {
112        self.reply_to = Some(request_id.into());
113        self
114    }
115}
116
117/// Turn a decoded frame back into wire JSON.
118///
119/// Written as a free function over the two body types rather than a trait,
120/// because there are exactly two and a trait would be indirection for its own
121/// sake.
122macro_rules! encoder {
123    ($body:ty, $name:ident) => {
124        /// Serialise this frame. Key order matches the fixtures, which matters
125        /// because `serde_json` here is built with `preserve_order`.
126        pub fn $name(frame: &Envelope<$body>) -> serde_json::Value {
127            serde_json::json!({
128                "v": frame.v,
129                "id": frame.id,
130                "type": frame.body.kind(),
131                "deviceId": frame.device_id,
132                "sentAt": frame.sent_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
133                "replyTo": frame.reply_to,
134                "payload": frame.body.payload(),
135            })
136        }
137    };
138}
139
140encoder!(DeviceBound, encode_device_bound);
141encoder!(PlatformBound, encode_platform_bound);
142
143/// Parse a frame the daemon received from the platform.
144pub fn parse_device_bound(
145    raw: &[u8],
146    now: DateTime<Utc>,
147) -> Result<Envelope<DeviceBound>, ProtocolError> {
148    let envelope = parse_header(raw)?;
149    let body = DeviceBound::parse(&envelope.kind, envelope.payload.clone())?;
150    // A command is a request. Answering one is the other direction's job, so a
151    // command that claims to answer something is a relay confusing itself.
152    if envelope.reply_to.is_some() {
153        return Err(ProtocolError::new(
154            ErrorCode::MalformedFrame,
155            "A command must not name a request it replies to.",
156        ));
157    }
158    finish(envelope, body, now)
159}
160
161/// Parse a frame the platform received from a daemon.
162pub fn parse_platform_bound(
163    raw: &[u8],
164    now: DateTime<Utc>,
165) -> Result<Envelope<PlatformBound>, ProtocolError> {
166    let envelope = parse_header(raw)?;
167    let body = PlatformBound::parse(&envelope.kind, envelope.payload.clone())?;
168    if body.requires_reply_to() != envelope.reply_to.is_some() {
169        return Err(ProtocolError::new(
170            ErrorCode::MalformedFrame,
171            "This event's `replyTo` does not match what its type requires.",
172        )
173        .with_detail(body.kind()));
174    }
175    finish(envelope, body, now)
176}
177
178/// Steps 1 to 3: length, shape, version.
179fn parse_header(raw: &[u8]) -> Result<RawEnvelope, ProtocolError> {
180    if raw.len() > limits::MAX_FRAME_BYTES {
181        return Err(ProtocolError::new(
182            ErrorCode::FrameTooLarge,
183            "Frame exceeds the protocol's maximum size.",
184        )
185        .with_detail(raw.len().to_string()));
186    }
187    let envelope: RawEnvelope = serde_json::from_slice(raw).map_err(|error| {
188        ProtocolError::new(ErrorCode::MalformedFrame, "Frame is not a valid envelope.")
189            .with_detail(error.to_string())
190    })?;
191    // Any version this build serves, not merely the newest one it prefers.
192    //
193    // These were the same number while 1 was the only version, and the
194    // difference only became visible when 2 arrived: an exact check would have
195    // made a v2 build reject every frame from a v1 daemon outright, which is
196    // precisely the peer that `SUPPORTED_VERSIONS`, `negotiate` and the whole
197    // degraded-session design exist to keep talking. Which *payloads* a session
198    // may use is settled by the version it negotiated; this is only about
199    // whether the envelope can be read at all.
200    if !SUPPORTED_VERSIONS.contains(&envelope.v) {
201        return Err(ProtocolError::new(
202            ErrorCode::UnsupportedProtocolVersion,
203            "This build does not speak that protocol version.",
204        )
205        .with_detail(envelope.v.to_string()));
206    }
207    check_identifier("id", &envelope.id)?;
208    check_identifier("deviceId", &envelope.device_id)?;
209    if let Some(reply_to) = &envelope.reply_to {
210        check_identifier("replyTo", reply_to)?;
211    }
212    Ok(envelope)
213}
214
215/// Step 7, then assembly.
216fn finish<T>(
217    envelope: RawEnvelope,
218    body: T,
219    now: DateTime<Utc>,
220) -> Result<Envelope<T>, ProtocolError> {
221    let age = now.signed_duration_since(envelope.sent_at);
222    let max_age = ChronoDuration::from_std(limits::MAX_REQUEST_AGE).expect("in range");
223    let max_ahead = ChronoDuration::from_std(limits::MAX_CLOCK_SKEW_AHEAD).expect("in range");
224    if age > max_age || age < -max_ahead {
225        return Err(ProtocolError::new(
226            ErrorCode::StaleRequest,
227            "Frame is outside the accepted time window.",
228        )
229        .with_detail(envelope.sent_at.to_rfc3339()));
230    }
231    Ok(Envelope {
232        v: envelope.v,
233        id: envelope.id,
234        device_id: envelope.device_id,
235        sent_at: envelope.sent_at,
236        reply_to: envelope.reply_to,
237        body,
238    })
239}
240
241/// An identifier is a name, not a field. Empty ones become ambiguous map keys
242/// and oversized ones become a payload channel that skips every payload check.
243fn check_identifier(field: &str, value: &str) -> Result<(), ProtocolError> {
244    if value.is_empty() {
245        return Err(
246            ProtocolError::new(ErrorCode::MalformedFrame, "Identifier is empty.")
247                .with_detail(field.to_string()),
248        );
249    }
250    if value.len() > MAX_IDENTIFIER_BYTES {
251        return Err(
252            ProtocolError::new(ErrorCode::MalformedFrame, "Identifier is too long.")
253                .with_detail(field.to_string()),
254        );
255    }
256    if !value
257        .bytes()
258        .all(|byte| byte.is_ascii_graphic() && byte != b'"')
259    {
260        return Err(ProtocolError::new(
261            ErrorCode::MalformedFrame,
262            "Identifier contains characters an identifier may not.",
263        )
264        .with_detail(field.to_string()));
265    }
266    Ok(())
267}