Skip to main content

monocoque_zmtp/
session.rs

1use crate::codec::{ZmtpDecoder, ZmtpError, ZmtpFrame};
2use crate::greeting::ZmtpGreeting;
3use crate::handshake::parse_ready_command;
4use bytes::{Bytes, BytesMut};
5use monocoque_core::buffer::SegmentedBuffer;
6
7/// Supported ZMQ socket types (no heap allocation)
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SocketType {
10    /// PAIR socket type.
11    Pair,
12    /// DEALER socket type.
13    Dealer,
14    /// ROUTER socket type.
15    Router,
16    /// PUB socket type.
17    Pub,
18    /// SUB socket type.
19    Sub,
20    /// REQ socket type.
21    Req,
22    /// REP socket type.
23    Rep,
24    /// PUSH socket type.
25    Push,
26    /// PULL socket type.
27    Pull,
28    /// XPUB socket type.
29    Xpub,
30    /// XSUB socket type.
31    Xsub,
32}
33
34impl SocketType {
35    /// Return the wire-format name string for this socket type.
36    #[must_use]
37    pub const fn as_str(&self) -> &'static str {
38        match self {
39            Self::Pair => "PAIR",
40            Self::Dealer => "DEALER",
41            Self::Router => "ROUTER",
42            Self::Pub => "PUB",
43            Self::Sub => "SUB",
44            Self::Req => "REQ",
45            Self::Rep => "REP",
46            Self::Push => "PUSH",
47            Self::Pull => "PULL",
48            Self::Xpub => "XPUB",
49            Self::Xsub => "XSUB",
50        }
51    }
52}
53
54/// Events emitted by the session (transport-agnostic)
55pub enum SessionEvent {
56    /// Send raw bytes immediately (greeting / handshake)
57    SendBytes(Bytes),
58
59    /// A validated ZMTP frame
60    Frame(ZmtpFrame),
61
62    /// Handshake completed successfully
63    HandshakeComplete {
64        /// Peer's ZMQ identity, if provided.
65        peer_identity: Option<Bytes>,
66        /// Socket type advertised by the peer.
67        peer_socket_type: SocketType,
68    },
69
70    /// Fatal protocol error
71    Error(ZmtpError),
72}
73
74enum State {
75    Greeting {
76        buffer: BytesMut,
77    },
78    Handshake {
79        decoder: ZmtpDecoder,
80        peer_socket_type: Option<SocketType>,
81        peer_identity: Option<Bytes>,
82    },
83    Active {
84        decoder: ZmtpDecoder,
85    },
86}
87
88/// Sans-IO ZMTP session
89pub struct ZmtpSession {
90    state: State,
91    local_socket_type: SocketType,
92    recv: SegmentedBuffer,
93    /// Resolved `max_msg_size` applied to every decoder this session creates.
94    /// `None` keeps the decoder's built-in default cap.
95    max_frame_size: Option<usize>,
96}
97
98/// Build a decoder honoring an optional `max_msg_size` limit.
99fn make_decoder(max_frame_size: Option<usize>) -> ZmtpDecoder {
100    max_frame_size.map_or_else(ZmtpDecoder::new, ZmtpDecoder::with_max_frame_size)
101}
102
103impl ZmtpSession {
104    /// Create a new ZMTP session starting in the greeting phase.
105    ///
106    /// Uses the decoder's built-in default frame-size cap. Use
107    /// [`Self::with_max_frame_size`] to enforce a custom `max_msg_size`.
108    #[must_use]
109    pub fn new(local_socket_type: SocketType) -> Self {
110        Self::with_max_frame_size(local_socket_type, None)
111    }
112
113    /// Create a session in the greeting phase that rejects frames whose declared
114    /// body length exceeds `max_frame_size` (the socket's `max_msg_size`).
115    ///
116    /// `None` keeps the decoder's built-in default cap.
117    #[must_use]
118    pub fn with_max_frame_size(
119        local_socket_type: SocketType,
120        max_frame_size: Option<usize>,
121    ) -> Self {
122        Self {
123            state: State::Greeting {
124                buffer: BytesMut::with_capacity(64),
125            },
126            local_socket_type,
127            recv: SegmentedBuffer::new(),
128            max_frame_size,
129        }
130    }
131
132    /// Create a session that's already past the handshake phase.
133    ///
134    /// Use this when handshake has been performed synchronously before
135    /// spawning the session actor.
136    #[must_use]
137    pub fn new_active(local_socket_type: SocketType) -> Self {
138        Self::new_active_with_max_frame_size(local_socket_type, None)
139    }
140
141    /// Create an already-active session that enforces `max_frame_size`
142    /// (the socket's `max_msg_size`) on its decoder.
143    ///
144    /// `None` keeps the decoder's built-in default cap.
145    #[must_use]
146    pub fn new_active_with_max_frame_size(
147        local_socket_type: SocketType,
148        max_frame_size: Option<usize>,
149    ) -> Self {
150        Self {
151            state: State::Active {
152                decoder: make_decoder(max_frame_size),
153            },
154            local_socket_type,
155            recv: SegmentedBuffer::new(),
156            max_frame_size,
157        }
158    }
159
160    /// Generate our greeting bytes
161    ///
162    /// # Compatibility
163    ///
164    /// Sends ZMTP 3.0 greeting for maximum backward compatibility with `ZeroMQ` 4.1+.
165    /// The implementation accepts any ZMTP 3.x version from peers, ensuring
166    /// compatibility with all modern ZMQ versions (4.1, 4.2, 4.3, 4.4).
167    pub fn local_greeting(&self) -> Bytes {
168        let mut b = BytesMut::with_capacity(64);
169
170        // Signature
171        b.extend_from_slice(&[0xFF]);
172        b.extend_from_slice(&[0u8; 8]);
173        b.extend_from_slice(&[0x7F]);
174
175        // Version 3.0 (backward compatible with all ZMQ 4.x)
176        b.extend_from_slice(&[0x03, 0x00]);
177
178        // Mechanism: NULL (Phase 1-3)
179        b.extend_from_slice(b"NULL");
180        b.extend_from_slice(&[0u8; 16]);
181
182        // As-server flag = 0 for NULL
183        b.extend_from_slice(&[0x00]);
184
185        // Padding
186        b.extend_from_slice(&[0u8; 31]);
187
188        b.freeze()
189    }
190
191    /// Feed incoming bytes into the session
192    pub fn on_bytes(&mut self, src: Bytes) -> Vec<SessionEvent> {
193        let mut events = Vec::new();
194
195        self.recv.push(src);
196
197        loop {
198            match &mut self.state {
199                // =========================
200                // Greeting
201                // =========================
202                State::Greeting { buffer } => {
203                    let needed = 64 - buffer.len();
204                    let take = needed.min(self.recv.len());
205                    if let Some(bytes) = self.recv.take_bytes(take) {
206                        buffer.extend_from_slice(&bytes);
207                    }
208
209                    if buffer.len() < 64 {
210                        break;
211                    }
212
213                    let greeting = buffer.split().freeze();
214
215                    match ZmtpGreeting::parse(&greeting) {
216                        Ok(_g) => {
217                            // Transition to handshake
218                            self.state = State::Handshake {
219                                decoder: make_decoder(self.max_frame_size),
220                                peer_socket_type: None,
221                                peer_identity: None,
222                            };
223
224                            // Send our greeting (if we haven't already)
225                            // Note: In connect scenario, greeting is sent first by us
226                            // In accept scenario, we send after receiving theirs
227                            // events.push(SessionEvent::SendBytes(self.local_greeting()));
228
229                            // Send READY command immediately after greeting exchange
230                            use crate::utils::{FLAG_COMMAND, build_ready, encode_frame};
231                            let socket_type_str = match self.local_socket_type {
232                                SocketType::Dealer => "DEALER",
233                                SocketType::Router => "ROUTER",
234                                SocketType::Pub => "PUB",
235                                SocketType::Sub => "SUB",
236                                SocketType::Xpub => "XPUB",
237                                SocketType::Xsub => "XSUB",
238                                SocketType::Req => "REQ",
239                                SocketType::Rep => "REP",
240                                SocketType::Push => "PUSH",
241                                SocketType::Pull => "PULL",
242                                SocketType::Pair => "PAIR",
243                            };
244                            let ready_body = build_ready(socket_type_str, None);
245                            let ready_frame = encode_frame(FLAG_COMMAND, &ready_body);
246                            events.push(SessionEvent::SendBytes(ready_frame));
247                        }
248                        Err(e) => {
249                            events.push(SessionEvent::Error(e));
250                            break;
251                        }
252                    }
253                }
254
255                // =========================
256                // Handshake
257                // =========================
258                State::Handshake {
259                    decoder,
260                    peer_socket_type,
261                    peer_identity,
262                } => {
263                    match decoder.decode(&mut self.recv) {
264                        Ok(Some(frame)) => {
265                            if !frame.is_command() {
266                                events.push(SessionEvent::Error(ZmtpError::Protocol));
267                                break;
268                            }
269
270                            let (parsed_socket_type, parsed_identity) =
271                                match parse_ready_command(&frame.payload) {
272                                    Ok(parsed) => parsed,
273                                    Err(e) => {
274                                        events.push(SessionEvent::Error(e));
275                                        break;
276                                    }
277                                };
278                            *peer_socket_type = Some(parsed_socket_type);
279                            *peer_identity = parsed_identity;
280
281                            // Extract values before transitioning state
282                            let peer_id = peer_identity.take();
283                            let peer_st = peer_socket_type.unwrap_or(self.local_socket_type);
284
285                            // Reuse the handshake decoder for the Active state; the
286                            // replacement is a throwaway needed only for mem::replace.
287                            let new_decoder = make_decoder(self.max_frame_size);
288                            let old_decoder = std::mem::replace(decoder, new_decoder);
289
290                            // Now transition state
291                            self.state = State::Active {
292                                decoder: old_decoder,
293                            };
294
295                            events.push(SessionEvent::HandshakeComplete {
296                                peer_identity: peer_id,
297                                peer_socket_type: peer_st,
298                            });
299                        }
300                        Ok(None) => break,
301                        Err(e) => {
302                            events.push(SessionEvent::Error(e));
303                            break;
304                        }
305                    }
306                }
307
308                // =========================
309                // Active
310                // =========================
311                State::Active { decoder } => match decoder.decode(&mut self.recv) {
312                    Ok(Some(frame)) => {
313                        events.push(SessionEvent::Frame(frame));
314                    }
315                    Ok(None) => break,
316                    Err(e) => {
317                        events.push(SessionEvent::Error(e));
318                        break;
319                    }
320                },
321            }
322        }
323
324        events
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use crate::utils::{FLAG_COMMAND, build_ready, encode_frame};
332
333    /// An active session created with a `max_msg_size` rejects a frame whose
334    /// declared body length exceeds the limit, before reading the body.
335    #[test]
336    fn active_session_enforces_max_frame_size() {
337        let mut session = ZmtpSession::new_active_with_max_frame_size(SocketType::Rep, Some(10));
338
339        // Short data frame header declaring a 20-byte body (flags=0x00, len=20),
340        // which is over the 10-byte limit. Only the 2-byte header is needed: the
341        // size check runs as soon as body_len is known.
342        let events = session.on_bytes(Bytes::from_static(&[0x00, 20]));
343
344        assert!(
345            events
346                .iter()
347                .any(|e| matches!(e, SessionEvent::Error(ZmtpError::SizeTooLarge))),
348            "oversized frame should produce a SizeTooLarge error, got {} events",
349            events.len()
350        );
351    }
352
353    /// The same frame decodes cleanly when it is within the configured limit.
354    #[test]
355    fn active_session_accepts_frame_within_limit() {
356        let mut session = ZmtpSession::new_active_with_max_frame_size(SocketType::Rep, Some(64));
357
358        // flags=0x00, len=2, body="hi"
359        let events = session.on_bytes(Bytes::from_static(&[0x00, 2, b'h', b'i']));
360
361        assert!(
362            events.iter().any(|e| matches!(e, SessionEvent::Frame(_))),
363            "frame within the limit should decode, got {} events",
364            events.len()
365        );
366    }
367
368    fn valid_null_greeting() -> Bytes {
369        let mut greeting = [0u8; 64];
370        greeting[0] = 0xFF;
371        greeting[9] = 0x7F;
372        greeting[10] = 0x03;
373        greeting[11] = 0x01;
374        greeting[12..16].copy_from_slice(b"NULL");
375        Bytes::copy_from_slice(&greeting)
376    }
377
378    fn input_with_handshake_command(command_body: Bytes) -> Bytes {
379        let command_frame = encode_frame(FLAG_COMMAND, &command_body);
380        let mut input = BytesMut::with_capacity(64 + command_frame.len());
381        input.extend_from_slice(&valid_null_greeting());
382        input.extend_from_slice(&command_frame);
383        input.freeze()
384    }
385
386    fn has_protocol_error(events: &[SessionEvent]) -> bool {
387        events
388            .iter()
389            .any(|event| matches!(event, SessionEvent::Error(ZmtpError::Protocol)))
390    }
391
392    fn handshake_complete(events: &[SessionEvent]) -> Option<(SocketType, Option<Bytes>)> {
393        events.iter().find_map(|event| match event {
394            SessionEvent::HandshakeComplete {
395                peer_socket_type,
396                peer_identity,
397            } => Some((*peer_socket_type, peer_identity.clone())),
398            _ => None,
399        })
400    }
401
402    #[test]
403    fn session_rejects_non_ready_command_during_handshake() {
404        let mut session = ZmtpSession::new(SocketType::Router);
405        let input = input_with_handshake_command(Bytes::from_static(b"\x04PING"));
406        let events = session.on_bytes(input);
407
408        assert!(has_protocol_error(&events));
409        assert!(handshake_complete(&events).is_none());
410    }
411
412    #[test]
413    fn session_rejects_ready_without_socket_type() {
414        let mut session = ZmtpSession::new(SocketType::Router);
415        let input = input_with_handshake_command(Bytes::from_static(b"\x05READY"));
416        let events = session.on_bytes(input);
417
418        assert!(has_protocol_error(&events));
419        assert!(handshake_complete(&events).is_none());
420    }
421
422    #[test]
423    fn session_uses_socket_type_and_identity_from_ready_metadata() {
424        let mut session = ZmtpSession::new(SocketType::Router);
425        let input = input_with_handshake_command(build_ready("DEALER", Some(b"client-1")));
426        let events = session.on_bytes(input);
427
428        let (peer_socket_type, peer_identity) =
429            handshake_complete(&events).expect("valid READY metadata should complete handshake");
430        assert_eq!(peer_socket_type, SocketType::Dealer);
431        assert_eq!(peer_identity.as_deref(), Some(&b"client-1"[..]));
432    }
433}