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}