Skip to main content

tightbeam/
router.rs

1#[cfg(not(feature = "std"))]
2extern crate alloc;
3
4use crate::{Frame, Message};
5
6#[cfg(feature = "derive")]
7use crate::Errorizable;
8
9/// Single `Arc` spelling for both std and no_std builds so [`routes!`]
10/// can emit one `dispatch` body via `$crate::router::Arc`.
11#[cfg(not(feature = "std"))]
12pub use alloc::sync::Arc;
13#[cfg(feature = "std")]
14pub use std::sync::Arc;
15
16pub type Result<T> = core::result::Result<T, RouterError>;
17
18#[cfg_attr(feature = "derive", derive(Errorizable))]
19#[derive(Debug)]
20pub enum RouterError {
21	#[cfg_attr(feature = "derive", error("No route configured for provided message"))]
22	UnknownRoute,
23	#[cfg_attr(
24		feature = "derive",
25		error("Frame body failed to decode as the dispatched type: {0}")
26	)]
27	DecodeFailed(crate::der::Error),
28	#[cfg_attr(feature = "derive", error("Frame body is encrypted; decrypt before routing"))]
29	ConfidentialFrame,
30	#[cfg_attr(feature = "derive", error("Frame body is compressed; inflate before routing"))]
31	CompressedFrame,
32}
33
34crate::impl_error_display!(RouterError {
35	UnknownRoute => "No route configured for provided message",
36	DecodeFailed(source) => "Frame body failed to decode as the dispatched type: {source}",
37	ConfidentialFrame => "Frame body is encrypted; decrypt before routing",
38	CompressedFrame => "Frame body is compressed; inflate before routing",
39});
40
41crate::impl_from!(crate::der::Error => RouterError::DecodeFailed);
42
43/// Reject frames whose body cannot be type-validated at routing time.
44///
45/// The router is strictly cleartext: an encrypted or compressed body
46/// cannot be decoded as the dispatched type, so validation would be
47/// impossible and misdelivery silent. Decrypt/inflate upstream, then
48/// route. Hand-written [`RouterPolicy`] impls MUST call this before
49/// decoding.
50///
51/// # Errors
52///
53/// - [`RouterError::ConfidentialFrame`] -- `metadata.confidentiality` is set.
54/// - [`RouterError::CompressedFrame`] -- `metadata.compactness` is set.
55pub fn ensure_cleartext(frame: &Frame) -> Result<()> {
56	if frame.metadata.confidentiality.is_some() {
57		return Err(RouterError::ConfidentialFrame);
58	}
59	if frame.metadata.compactness.is_some() {
60		return Err(RouterError::CompressedFrame);
61	}
62
63	Ok(())
64}
65
66pub trait RouterPolicy: Send + Sync {
67	/// Route `frame` to the handler registered for message type `T`,
68	/// delivering the body decoded as `T`.
69	///
70	/// Dispatch decodes the cleartext body exactly once and hands the
71	/// typed value to the handler, so a mismatched turbofish fails
72	/// loudly at the dispatch site instead of silently delivering
73	/// foreign bytes. Opaque payloads are rejected before any decode
74	/// attempt (see [`ensure_cleartext`]).
75	///
76	/// Residual: two DER-structurally-identical types still cross-decode
77	/// (the wire format carries no type discriminator by design -- the
78	/// receiver decides the type, never the sender). [`routes!`] keeps
79	/// each type adjacent to its handler to confine that risk.
80	///
81	/// # Errors
82	///
83	/// - [`RouterError::ConfidentialFrame`] -- body is encrypted.
84	/// - [`RouterError::CompressedFrame`] -- body is compressed.
85	/// - [`RouterError::DecodeFailed`] -- body did not decode as `T`.
86	/// - [`RouterError::UnknownRoute`] -- no handler registered for `T`.
87	fn dispatch<T: Message + Send + 'static>(&self, frame: Arc<Frame>) -> Result<()>;
88}
89
90/// Declare a router struct and its [`RouterPolicy`] impl.
91///
92/// Each route pairs a message type with a handler receiving
93/// `|router, frame, msg|`: the router's fields, the original
94/// [`Frame`] (metadata access), and the body already decoded as the
95/// registered type.
96#[macro_export]
97macro_rules! routes {
98	(
99		$RouterName:ident { $( $field:ident : $fty:ty ),* $(,)? } :
100		$(
101			$MsgTy:ty | $router:ident, $frame:ident, $msg:ident | $handler:block
102		)+
103	) => {
104		struct $RouterName { $( $field : $fty ),* }
105
106		impl $crate::router::RouterPolicy for $RouterName {
107			fn dispatch<T: $crate::Message + Send + 'static>(
108				&self,
109				frame: $crate::router::Arc<$crate::Frame>,
110			) -> $crate::router::Result<()> {
111				$crate::router::ensure_cleartext(&frame)?;
112				$(
113					if core::any::TypeId::of::<T>() == core::any::TypeId::of::<$MsgTy>() {
114						let decoded: $MsgTy = $crate::der::Decode::from_der(frame.message.as_slice())?;
115						let ($router, $frame, $msg) = (self, frame, decoded);
116						{ $handler }
117						return Ok(());
118					}
119				)*
120				Err($crate::router::RouterError::UnknownRoute)
121			}
122		}
123	};
124}
125
126#[cfg(all(test, feature = "builder"))]
127mod tests {
128	use std::sync::{mpsc, Arc};
129	use std::time::Duration;
130
131	use crate::cms::compressed_data::CompressedData;
132	use crate::cms::content_info::CmsVersion;
133	use crate::cms::enveloped_data::EncryptedContentInfo;
134	use crate::cms::signed_data::EncapsulatedContentInfo;
135	use crate::der::asn1::OctetString;
136	use crate::der::{Decode, Encode, Sequence};
137	use crate::oids::{COMPRESSION_ZSTD, DATA};
138	use crate::router::{RouterError, RouterPolicy};
139	use crate::spki::AlgorithmIdentifier;
140	use crate::Beamable;
141	use crate::Frame;
142
143	#[derive(Beamable, Sequence, Clone, Debug, PartialEq)]
144	pub struct HealthCheck {
145		pub uptime: u64,
146	}
147
148	#[derive(Beamable, Sequence, Clone, Debug, PartialEq)]
149	pub struct Payment {
150		pub from: String,
151		pub amount: u64,
152	}
153
154	#[derive(Beamable, Sequence, Clone, Debug, PartialEq)]
155	pub struct Unrouted {
156		pub note: String,
157	}
158
159	type Delivery<T> = (Arc<Frame>, T);
160
161	routes! {
162		ChannelRouter {
163			payment_tx: mpsc::Sender<Delivery<Payment>>,
164			health_tx: mpsc::Sender<Delivery<HealthCheck>>,
165		}:
166			Payment |router, frame, msg| {
167				let _ = router.payment_tx.send((frame, msg));
168			}
169			HealthCheck |router, frame, msg| {
170				let _ = router.health_tx.send((frame, msg));
171			}
172	}
173
174	fn build_router() -> (
175		ChannelRouter,
176		mpsc::Receiver<Delivery<Payment>>,
177		mpsc::Receiver<Delivery<HealthCheck>>,
178	) {
179		let (payment_tx, payment_rx) = mpsc::channel();
180		let (health_tx, health_rx) = mpsc::channel();
181		(ChannelRouter { payment_tx, health_tx }, payment_rx, health_rx)
182	}
183
184	fn compose_payment(index: u64) -> Result<Frame, Box<dyn std::error::Error>> {
185		let frame = compose! {
186			V0: id: format!("p-{index}"),
187				order: 1u64,
188				message: Payment {
189					from: "alice".into(),
190					amount: index
191				}
192		}?;
193		Ok(frame)
194	}
195
196	fn compose_health(index: u64) -> Result<Frame, Box<dyn std::error::Error>> {
197		let frame = compose! {
198			V0: id: format!("h-{index}"),
199				order: 1u64,
200				message: HealthCheck { uptime: index }
201		}?;
202		Ok(frame)
203	}
204
205	#[test]
206	fn dispatch_delivers_decoded_message_per_route() -> Result<(), Box<dyn std::error::Error>> {
207		let (router, payment_rx, health_rx) = build_router();
208
209		let n = 5u64;
210		for i in 0..n {
211			router.dispatch::<Payment>(Arc::new(compose_payment(i)?))?;
212			router.dispatch::<HealthCheck>(Arc::new(compose_health(i)?))?;
213		}
214
215		let timeout = Duration::from_millis(200);
216		for i in 0..n {
217			let (payment_frame, payment) = payment_rx.recv_timeout(timeout)?;
218			assert_eq!(&payment_frame.metadata.id, &format!("p-{i}").as_bytes());
219			assert_eq!(payment, Payment { from: "alice".into(), amount: i });
220
221			let (health_frame, health) = health_rx.recv_timeout(timeout)?;
222			assert_eq!(&health_frame.metadata.id, &format!("h-{i}").as_bytes());
223			assert_eq!(health, HealthCheck { uptime: i });
224		}
225
226		Ok(())
227	}
228
229	#[test]
230	fn dispatch_rejects_misdelivered_type() -> Result<(), Box<dyn std::error::Error>> {
231		let (router, payment_rx, _health_rx) = build_router();
232
233		let result = router.dispatch::<Payment>(Arc::new(compose_health(0)?));
234		assert!(matches!(result, Err(RouterError::DecodeFailed(_))));
235		assert!(matches!(
236			payment_rx.recv_timeout(Duration::from_millis(50)),
237			Err(mpsc::RecvTimeoutError::Timeout)
238		));
239		Ok(())
240	}
241
242	#[test]
243	fn dispatch_rejects_unregistered_type() -> Result<(), Box<dyn std::error::Error>> {
244		let (router, _payment_rx, _health_rx) = build_router();
245
246		let result = router.dispatch::<Unrouted>(Arc::new(compose_payment(0)?));
247		assert!(matches!(result, Err(RouterError::UnknownRoute)));
248		Ok(())
249	}
250
251	#[test]
252	fn dispatch_rejects_confidential_frame() -> Result<(), Box<dyn std::error::Error>> {
253		let (router, payment_rx, _health_rx) = build_router();
254
255		let mut frame = compose_payment(0)?;
256		frame.metadata.confidentiality = Some(EncryptedContentInfo {
257			content_type: DATA,
258			content_enc_alg: AlgorithmIdentifier { oid: DATA, parameters: None },
259			encrypted_content: Some(OctetString::new(vec![0; 16])?),
260		});
261
262		let result = router.dispatch::<Payment>(Arc::new(frame));
263		assert!(matches!(result, Err(RouterError::ConfidentialFrame)));
264		assert!(matches!(
265			payment_rx.recv_timeout(Duration::from_millis(50)),
266			Err(mpsc::RecvTimeoutError::Timeout)
267		));
268		Ok(())
269	}
270
271	#[test]
272	fn dispatch_rejects_compressed_frame() -> Result<(), Box<dyn std::error::Error>> {
273		let (router, _payment_rx, _health_rx) = build_router();
274
275		let mut frame = compose_payment(0)?;
276		frame.metadata.compactness = Some(CompressedData {
277			version: CmsVersion::V0,
278			compression_alg: AlgorithmIdentifier { oid: COMPRESSION_ZSTD, parameters: None },
279			encap_content_info: EncapsulatedContentInfo {
280				econtent_type: DATA,
281				econtent: Some(crate::der::Any::from_der(&OctetString::new(vec![0; 8])?.to_der()?)?),
282			},
283		});
284
285		let result = router.dispatch::<Payment>(Arc::new(frame));
286		assert!(matches!(result, Err(RouterError::CompressedFrame)));
287		Ok(())
288	}
289}