Skip to main content

confium_daemon/
dispatch.rs

1//! Method dispatch table.
2//!
3//! Each JSON-RPC method name is mapped to a handler function. The
4//! handler receives the parsed [`RpcRequest`] and a reference to the
5//! daemon-owned [`Confium`] instance. Handlers return a
6//! JSON-serializable result value or a [`RpcError`].
7//!
8//! Adding a new method is a single entry in [`Dispatch::new`]:
9//! register the name and the handler. No match arm, no central switch.
10//! This is the open/closed shape the daemon aims for — extension is
11//! registration, not modification.
12
13use std::collections::HashMap;
14use std::rc::Rc;
15
16use serde_json::Value;
17
18use crate::error::RpcError;
19use crate::methods;
20use crate::server::SharedConfium;
21
22/// Type alias for a handler. Handlers are async so they can await
23/// slow operations (audit subscription setup, keystore I/O, etc.)
24/// without blocking other connections.
25///
26/// The handler is `!Send` because `Confium` is `!Send` (plugin
27/// interfaces hold `Rc<dyn Any>`). The entire dispatch + connection
28/// loop runs on a single [`tokio::task::LocalSet`].
29pub type Handler = Rc<
30    dyn Fn(
31        SharedConfium,
32        Value,
33    ) -> std::pin::Pin<
34        Box<dyn std::future::Future<Output = std::result::Result<Value, RpcError>>>,
35    >,
36>;
37
38/// The dispatch table. Maps method name → handler.
39pub struct Dispatch {
40    table: HashMap<String, Handler>,
41}
42
43impl Dispatch {
44    /// Build the full dispatch table. New methods are added here.
45    pub fn new() -> Self {
46        let mut table = HashMap::new();
47
48        // -- meta --
49        table.insert("version".to_string(), rc(methods::meta::version));
50        table.insert("shutdown".to_string(), rc(methods::meta::shutdown));
51
52        // -- plugin --
53        table.insert("plugin_load".to_string(), rc(methods::plugin::plugin_load));
54        table.insert(
55            "plugin_unload".to_string(),
56            rc(methods::plugin::plugin_unload),
57        );
58        table.insert("plugin_list".to_string(), rc(methods::plugin::plugin_list));
59
60        // -- hash --
61        table.insert("hash_create".to_string(), rc(methods::hash::hash_create));
62        table.insert("hash_update".to_string(), rc(methods::hash::hash_update));
63        table.insert(
64            "hash_finalize".to_string(),
65            rc(methods::hash::hash_finalize),
66        );
67
68        // -- cipher --
69        table.insert(
70            "cipher_create".to_string(),
71            rc(methods::cipher::cipher_create),
72        );
73        table.insert(
74            "cipher_update".to_string(),
75            rc(methods::cipher::cipher_update),
76        );
77        table.insert(
78            "cipher_finalize".to_string(),
79            rc(methods::cipher::cipher_finalize),
80        );
81
82        // -- aead --
83        table.insert("aead_create".to_string(), rc(methods::aead::aead_create));
84        table.insert(
85            "aead_encrypt_update".to_string(),
86            rc(methods::aead::aead_encrypt_update),
87        );
88        table.insert(
89            "aead_decrypt_update".to_string(),
90            rc(methods::aead::aead_decrypt_update),
91        );
92        table.insert(
93            "aead_finalize".to_string(),
94            rc(methods::aead::aead_finalize),
95        );
96
97        // -- kdf --
98        table.insert("kdf_create".to_string(), rc(methods::kdf::kdf_create));
99        table.insert("kdf_derive".to_string(), rc(methods::kdf::kdf_derive));
100
101        // -- rng --
102        table.insert("rng_create".to_string(), rc(methods::rng::rng_create));
103        table.insert("rng_generate".to_string(), rc(methods::rng::rng_generate));
104
105        // -- signature --
106        table.insert(
107            "signature_keypair_generate".to_string(),
108            rc(methods::signature::signature_keypair_generate),
109        );
110        table.insert(
111            "signature_signer_update".to_string(),
112            rc(methods::signature::signature_signer_update),
113        );
114        table.insert(
115            "signature_signer_finalize".to_string(),
116            rc(methods::signature::signature_signer_finalize),
117        );
118        table.insert(
119            "signature_verifier_update".to_string(),
120            rc(methods::signature::signature_verifier_update),
121        );
122        table.insert(
123            "signature_verifier_finalize".to_string(),
124            rc(methods::signature::signature_verifier_finalize),
125        );
126
127        // -- kem --
128        table.insert(
129            "kem_keypair_generate".to_string(),
130            rc(methods::kem::kem_keypair_generate),
131        );
132        table.insert(
133            "kem_encapsulate".to_string(),
134            rc(methods::kem::kem_encapsulate),
135        );
136        table.insert(
137            "kem_decapsulate".to_string(),
138            rc(methods::kem::kem_decapsulate),
139        );
140
141        // -- keyfmt --
142        table.insert(
143            "keyfmt_parse".to_string(),
144            rc(methods::keyfmt::keyfmt_parse),
145        );
146        table.insert(
147            "keyfmt_serialize".to_string(),
148            rc(methods::keyfmt::keyfmt_serialize),
149        );
150
151        // -- keystore --
152        table.insert(
153            "keystore_create".to_string(),
154            rc(methods::keystore::keystore_create),
155        );
156        table.insert(
157            "keystore_put_secret".to_string(),
158            rc(methods::keystore::keystore_put_secret),
159        );
160        table.insert(
161            "keystore_get_secret".to_string(),
162            rc(methods::keystore::keystore_get_secret),
163        );
164
165        // -- tc (threshold computing) --
166        table.insert(
167            "tc_session_create".to_string(),
168            rc(methods::tc::tc_session_create),
169        );
170        table.insert(
171            "tc_session_round".to_string(),
172            rc(methods::tc::tc_session_round),
173        );
174        table.insert(
175            "tc_session_result".to_string(),
176            rc(methods::tc::tc_session_result),
177        );
178
179        // -- registry --
180        table.insert(
181            "registry_install".to_string(),
182            rc(methods::registry::registry_install),
183        );
184        table.insert(
185            "registry_search".to_string(),
186            rc(methods::registry::registry_search),
187        );
188
189        // -- audit --
190        table.insert(
191            "audit_subscribe".to_string(),
192            rc(methods::audit::audit_subscribe),
193        );
194
195        // -- composite (stateless verifier) --
196        table.insert(
197            "composite_verify".to_string(),
198            rc(methods::composite::composite_verify),
199        );
200
201        // -- attributes (stateless predicate evaluator) --
202        table.insert(
203            "attributes_evaluate".to_string(),
204            rc(methods::attributes::attributes_evaluate),
205        );
206
207        Dispatch { table }
208    }
209
210    /// Look up the handler for `method`. Returns `None` for unknown
211    /// methods — the caller produces a `MethodNotFound` error.
212    pub fn get(&self, method: &str) -> Option<&Handler> {
213        self.table.get(method)
214    }
215
216    /// Iterate over registered method names. Used by the `version`
217    /// handler and for diagnostics.
218    pub fn methods(&self) -> impl Iterator<Item = &str> {
219        self.table.keys().map(String::as_str)
220    }
221}
222
223impl Default for Dispatch {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// Wrap a bare handler function into the type-erased `Handler` shape.
230/// The handler signature is `async fn(cfm, params) -> Result<Value, RpcError>`.
231fn rc<F, Fut>(f: F) -> Handler
232where
233    F: Fn(SharedConfium, Value) -> Fut + 'static,
234    Fut: std::future::Future<Output = std::result::Result<Value, RpcError>> + 'static,
235{
236    Rc::new(move |cfm, params| Box::pin(f(cfm, params)))
237}
238
239/// Extract a typed parameter struct from the raw `Value`, producing an
240/// `InvalidParams` RPC error on failure.
241pub fn parse_params<T: serde::de::DeserializeOwned>(
242    params: &Value,
243) -> std::result::Result<T, RpcError> {
244    serde_json::from_value(params.clone()).map_err(|e| RpcError::InvalidParams {
245        detail: e.to_string(),
246    })
247}
248
249/// Require that `params` is an object (or unit / missing). Returns the
250/// cloned map so handlers can pull fields with `.get()`.
251pub fn params_object(
252    params: &Value,
253) -> std::result::Result<serde_json::Map<String, Value>, RpcError> {
254    match params {
255        Value::Object(map) => Ok(map.clone()),
256        Value::Null => Ok(serde_json::Map::new()),
257        _ => Err(RpcError::InvalidParams {
258            detail: "expected an object".to_string(),
259        }),
260    }
261}
262
263/// Pull a string field from the params map, erroring if missing.
264pub fn require_str(
265    map: &serde_json::Map<String, Value>,
266    key: &str,
267) -> std::result::Result<String, RpcError> {
268    map.get(key)
269        .and_then(|v| v.as_str())
270        .map(|s| s.to_string())
271        .ok_or_else(|| RpcError::InvalidParams {
272            detail: format!("missing or non-string field '{key}'"),
273        })
274}
275
276/// Pull a byte field from the params map. Accepts a JSON string and
277/// decodes it as base64, or a JSON array of numbers.
278pub fn require_bytes(
279    map: &serde_json::Map<String, Value>,
280    key: &str,
281) -> std::result::Result<Vec<u8>, RpcError> {
282    let v = map.get(key).ok_or_else(|| RpcError::InvalidParams {
283        detail: format!("missing field '{key}'"),
284    })?;
285    match v {
286        Value::String(s) => decode_base64(s).map_err(|e| RpcError::InvalidParams {
287            detail: format!("field '{key}' is not valid base64: {e}"),
288        }),
289        Value::Array(arr) => {
290            let mut out = Vec::with_capacity(arr.len());
291            for (i, n) in arr.iter().enumerate() {
292                let b = n.as_u64().ok_or_else(|| RpcError::InvalidParams {
293                    detail: format!("field '{key}[{i}]' is not a number"),
294                })?;
295                if b > 255 {
296                    return Err(RpcError::InvalidParams {
297                        detail: format!("field '{key}[{i}]' = {b} exceeds 255"),
298                    });
299                }
300                out.push(b as u8);
301            }
302            Ok(out)
303        }
304        _ => Err(RpcError::InvalidParams {
305            detail: format!("field '{key}' must be a base64 string or byte array"),
306        }),
307    }
308}
309
310/// Standard base64 decoder (RFC 4648, no padding required). Kept
311/// inline to avoid pulling a base64 crate into the workspace.
312fn decode_base64(s: &str) -> std::result::Result<Vec<u8>, String> {
313    let s = s.trim_end_matches('=');
314    let mut out = Vec::with_capacity(s.len() * 3 / 4);
315    let mut buf: u32 = 0;
316    let mut bits: u32 = 0;
317    for (i, c) in s.chars().enumerate() {
318        let val = match c {
319            'A'..='Z' => c as u32 - 'A' as u32,
320            'a'..='z' => c as u32 - 'a' as u32 + 26,
321            '0'..='9' => c as u32 - '0' as u32 + 52,
322            '+' | '-' => 62,
323            '/' | '_' => 63,
324            _ => return Err(format!("invalid base64 char at index {i}")),
325        };
326        buf = (buf << 6) | val;
327        bits += 6;
328        if bits >= 8 {
329            bits -= 8;
330            out.push((buf >> bits) as u8);
331            buf &= (1 << bits) - 1;
332        }
333    }
334    Ok(out)
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn base64_decode_roundtrip() {
343        assert_eq!(decode_base64("aGVsbG8=").unwrap(), b"hello");
344        assert_eq!(decode_base64("Zm9vYmFy").unwrap(), b"foobar");
345        // URL-safe variant
346        assert_eq!(decode_base64("-_8=").unwrap(), vec![0xfb, 0xff]);
347    }
348
349    #[test]
350    fn dispatch_registers_all_methods() {
351        let d = Dispatch::new();
352        let names: Vec<&str> = d.methods().collect();
353        assert!(names.contains(&"version"));
354        assert!(names.contains(&"shutdown"));
355        assert!(names.contains(&"plugin_load"));
356        assert!(names.contains(&"hash_create"));
357        assert!(names.contains(&"audit_subscribe"));
358    }
359}