Skip to main content

melin_app/
decoder.rs

1//! Wire-side request decoder seam.
2//!
3//! The server runtime (accept loop, frame reader, DPDK transport)
4//! consumes incoming frames from the network and needs to turn them
5//! into application events to publish to the pipeline. The decoding
6//! itself — telling the application's messages apart, mapping
7//! per-variant fields, enforcing per-connection role policy — is
8//! application-shaped: a trading server decodes order submissions, a
9//! payments server decodes transfers, a logistics server decodes
10//! shipment events. This trait is the seam that lets the runtime
11//! delegate that decoding to the application without ever naming the
12//! concrete wire enum.
13//!
14//! The frame around the request is not the application's. Every client
15//! frame is `[length: u32 LE][tag: u8][body]`, and the tag is the
16//! protocol's: the runtime reads it, handles or drops the protocol's own
17//! frames, and hands the decoder the body of an application frame alone.
18//! The body's layout is the application's from its first byte, with no
19//! value reserved.
20//!
21//! The runtime calls
22//! [`RequestDecoder::decode`](crate::decoder::RequestDecoder::decode) once
23//! per such frame; the [`Decoded`](crate::decoder::Decoded) return value
24//! encodes exactly the four outcomes the runtime acts on (drop, publish,
25//! reject with reason, log decode error).
26
27use tracing::error;
28
29use crate::AppEvent;
30use crate::auth::{AuthorizedKeys, ClientRole, Role, RoleId};
31
32/// Decode an authenticated client request into an application event the
33/// runtime can publish to the pipeline.
34///
35/// Stateless on the connection (the runtime carries connection-level
36/// state — the client's role, `key_hash` — and feeds the relevant piece
37/// in per call). Implementors are typically zero-sized types.
38pub trait RequestDecoder: Send + Sync {
39    /// Application event type produced on a successful decode. The
40    /// runtime wraps this in a transport-level envelope (e.g.
41    /// `JournalEvent::App`) before publishing.
42    type Event: AppEvent;
43
44    /// The application's client roles, named by tokens in the
45    /// `authorized_keys` file. [`NoRoles`](crate::auth::NoRoles) for an
46    /// application that admits operator keys only.
47    type Role: Role;
48
49    /// Decode one request. `body` is the application frame's body, up to
50    /// the end of the frame, and may be empty. `role` is the role of the
51    /// key the connection authenticated with: the runtime's operator, or
52    /// one of the application's own.
53    fn decode(&self, body: &[u8], role: ClientRole<Self::Role>) -> Decoded<Self::Event>;
54}
55
56/// Outcome of a single [`RequestDecoder::decode`] call. The runtime
57/// branches on this and never needs to know the underlying wire enum.
58pub enum Decoded<E: AppEvent> {
59    /// Drop the request silently. For application messages that are not
60    /// events — a subscription request arriving on a connection that
61    /// only submits events, say. The protocol's own frames never reach the
62    /// decoder.
63    Filter,
64    /// Request OK and authorized. The runtime publishes the event. Whether
65    /// it needs a timestamp is derived by the runtime from
66    /// [`AppEvent::is_query`] — query events bypass the journal and skip
67    /// the wall-clock stamp.
68    Permitted(E),
69    /// The connection's role may not make this request. The static string
70    /// is logged at debug level on the reader thread; the runtime drops
71    /// the request.
72    PermissionDenied(&'static str),
73    /// Decode failure (unknown message, malformed body, invalid field). The
74    /// runtime logs at debug level and drops the request; the connection
75    /// is not closed (a misbehaving client drops itself on the next read
76    /// timeout).
77    DecodeError(&'static str),
78}
79
80mod sealed {
81    /// Keeps [`ErasedDecoder`](super::ErasedDecoder) implemented by its
82    /// blanket impl alone.
83    pub trait Sealed {}
84}
85
86impl<D: RequestDecoder> sealed::Sealed for D {}
87
88/// A [`RequestDecoder`] as the runtime holds it: with its role type
89/// erased, so nothing in the runtime names the application's roles.
90///
91/// Implemented for every `RequestDecoder`, and by nothing else. An
92/// application implements `RequestDecoder` and never calls this.
93pub trait ErasedDecoder<E: AppEvent>: sealed::Sealed + Send + Sync {
94    /// Turn `role` back into the decoder's own role type and decode `body`
95    /// as [`RequestDecoder::decode`] does.
96    ///
97    /// An application role index outside the decoder's table can only be
98    /// a bug (a keys table parsed for another role type), never client
99    /// input. It is logged as an error and the request is refused without
100    /// reaching the decoder: failing closed, where indexing would panic the
101    /// reader thread. The runtime's pairing check ([`Self::matches_keys`])
102    /// keeps it unreachable.
103    fn decode_erased(&self, body: &[u8], role: ClientRole<RoleId>) -> Decoded<E>;
104
105    /// Whether `keys` was parsed for this decoder's role type, so the
106    /// indices it hands out name this decoder's roles.
107    fn matches_keys(&self, keys: &AuthorizedKeys) -> bool;
108}
109
110impl<D: RequestDecoder> ErasedDecoder<D::Event> for D {
111    fn decode_erased(&self, body: &[u8], role: ClientRole<RoleId>) -> Decoded<D::Event> {
112        let role = match role {
113            ClientRole::Operator => ClientRole::Operator,
114            ClientRole::App(id) => match D::Role::ROLES.get(id.index()) {
115                Some(&(_, role)) => ClientRole::App(role),
116                None => {
117                    error!(
118                        index = id.index(),
119                        roles = D::Role::ROLES.len(),
120                        "role index outside the decoder's role table; request refused"
121                    );
122                    return Decoded::PermissionDenied("role index outside the role table");
123                }
124            },
125        };
126        self.decode(body, role)
127    }
128
129    fn matches_keys(&self, keys: &AuthorizedKeys) -> bool {
130        keys.is_for::<D::Role>()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use std::sync::Mutex;
137
138    use super::*;
139    use crate::CodecError;
140    use crate::auth::tests::DeskRole;
141    use crate::auth::{KeyRole, NoRoles};
142
143    /// Minimal event: decoding produces this for every request.
144    #[derive(Debug, Clone, Copy)]
145    struct Unit;
146
147    impl AppEvent for Unit {
148        const MAX_ENCODED_SIZE: usize = 0;
149
150        fn encoded_size(&self) -> usize {
151            0
152        }
153        fn encode(&self, _buf: &mut [u8]) -> usize {
154            0
155        }
156        fn decode(_buf: &[u8]) -> Result<Self, CodecError> {
157            Ok(Unit)
158        }
159        fn is_query(&self) -> bool {
160            false
161        }
162    }
163
164    /// Records the role of every request it decodes, and permits it.
165    struct Recording<R: Role> {
166        seen: Mutex<Vec<ClientRole<R>>>,
167    }
168
169    impl<R: Role> Recording<R> {
170        fn new() -> Self {
171            Recording {
172                seen: Mutex::new(Vec::new()),
173            }
174        }
175        fn seen(&self) -> Vec<ClientRole<R>> {
176            self.seen.lock().unwrap().clone()
177        }
178    }
179
180    impl<R: Role> RequestDecoder for Recording<R> {
181        type Event = Unit;
182        type Role = R;
183
184        fn decode(&self, _body: &[u8], role: ClientRole<R>) -> Decoded<Unit> {
185            self.seen.lock().unwrap().push(role);
186            Decoded::Permitted(Unit)
187        }
188    }
189
190    /// The client role a one-line keys file for `R` gives `token`.
191    fn client_role_of<R: Role>(token: &str) -> ClientRole<RoleId> {
192        let key = [0x11u8; 32];
193        let listed = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key);
194        let keys = AuthorizedKeys::parse::<R>(&format!("{token} {listed} test\n")).unwrap();
195        keys.lookup(&key)
196            .and_then(KeyRole::client)
197            .expect("a client role")
198    }
199
200    /// Every application role, listed in a file, parsed, looked up and
201    /// passed through the erased decoder, reaches the typed decoder as
202    /// exactly that role; the operator as the operator.
203    #[test]
204    fn every_role_reaches_the_decoder_as_itself() {
205        let decoder = Recording::<DeskRole>::new();
206        let mut expected = vec![ClientRole::Operator];
207        decoder.decode_erased(b"", client_role_of::<DeskRole>("operator"));
208        for &(token, role) in DeskRole::ROLES {
209            decoder.decode_erased(b"", client_role_of::<DeskRole>(token));
210            expected.push(ClientRole::App(role));
211        }
212        assert_eq!(decoder.seen(), expected);
213    }
214
215    /// An index past the decoder's table fails closed, without reaching
216    /// the decoder. The only way to get one is a table parsed for a larger
217    /// role type, handed straight to the erased decoder: at this layer
218    /// there is no pairing check, which lives in the runtime's entry
219    /// chain.
220    #[test]
221    fn an_index_outside_the_role_table_is_refused_without_decoding() {
222        let past_the_end = client_role_of::<DeskRole>("readonly");
223        let decoder = Recording::<NoRoles>::new();
224        assert!(matches!(
225            decoder.decode_erased(b"", past_the_end),
226            Decoded::PermissionDenied(_)
227        ));
228        assert!(decoder.seen().is_empty());
229    }
230
231    #[test]
232    fn a_decoder_matches_only_keys_parsed_for_its_role_type() {
233        let decoder = Recording::<DeskRole>::new();
234        assert!(decoder.matches_keys(&AuthorizedKeys::parse::<DeskRole>("").unwrap()));
235        assert!(!decoder.matches_keys(&AuthorizedKeys::parse::<NoRoles>("").unwrap()));
236    }
237}