Skip to main content

moq_token/
claims.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use serde_with::{TimestampSeconds, serde_as};
3
4fn is_false(value: &bool) -> bool {
5	!value
6}
7
8fn string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
9where
10	D: Deserializer<'de>,
11{
12	#[derive(Deserialize)]
13	#[serde(untagged)]
14	enum StringOrVec {
15		String(String),
16		Vec(Vec<String>),
17	}
18
19	match StringOrVec::deserialize(deserializer)? {
20		StringOrVec::String(s) => Ok(vec![s]),
21		StringOrVec::Vec(v) => Ok(v),
22	}
23}
24
25#[serde_with::skip_serializing_none]
26#[serde_as]
27#[derive(Debug, Serialize, Deserialize, Default, Clone)]
28#[serde(default)]
29pub struct Claims {
30	/// The root for the publish/subscribe options below.
31	/// It's mostly for compression and is optional, defaulting to the empty string.
32	#[serde(default, rename = "root", skip_serializing_if = "String::is_empty")]
33	pub root: String,
34
35	/// If specified, the user can publish any matching broadcasts.
36	/// If not specified, the user will not publish any broadcasts.
37	#[serde(
38		default,
39		rename = "put",
40		skip_serializing_if = "Vec::is_empty",
41		deserialize_with = "string_or_vec"
42	)]
43	pub publish: Vec<String>,
44
45	/// If true, then this client is considered a cluster node.
46	/// Both the client and server will only announce broadcasts from non-cluster clients.
47	/// This avoids convoluted routing, as only the primary origin will announce.
48	//
49	// TODO This shouldn't be part of the token.
50	#[serde(default, rename = "cluster", skip_serializing_if = "is_false")]
51	pub cluster: bool,
52
53	/// If specified, the user can subscribe to any matching broadcasts.
54	/// If not specified, the user will not receive announcements and cannot subscribe to any broadcasts.
55	// NOTE: This can't be renamed to "sub" because that's a reserved JWT field.
56	#[serde(
57		default,
58		rename = "get",
59		skip_serializing_if = "Vec::is_empty",
60		deserialize_with = "string_or_vec"
61	)]
62	pub subscribe: Vec<String>,
63
64	/// The expiration time of the token as a unix timestamp.
65	#[serde(rename = "exp")]
66	#[serde_as(as = "Option<TimestampSeconds<i64>>")]
67	pub expires: Option<std::time::SystemTime>,
68
69	/// The issued time of the token as a unix timestamp.
70	#[serde(rename = "iat")]
71	#[serde_as(as = "Option<TimestampSeconds<i64>>")]
72	pub issued: Option<std::time::SystemTime>,
73}
74
75impl Claims {
76	pub fn validate(&self) -> anyhow::Result<()> {
77		if self.publish.is_empty() && self.subscribe.is_empty() {
78			anyhow::bail!("no publish or subscribe allowed; token is useless");
79		}
80
81		Ok(())
82	}
83}
84
85#[cfg(test)]
86mod tests {
87	use super::*;
88
89	use std::time::{Duration, SystemTime};
90
91	fn create_test_claims() -> Claims {
92		Claims {
93			root: "test-path".to_string(),
94			publish: vec!["test-pub".into()],
95			cluster: false,
96			subscribe: vec!["test-sub".into()],
97			expires: Some(SystemTime::now() + Duration::from_secs(3600)),
98			issued: Some(SystemTime::now()),
99		}
100	}
101
102	#[test]
103	fn test_claims_validation_success() {
104		let claims = create_test_claims();
105		assert!(claims.validate().is_ok());
106	}
107
108	#[test]
109	fn test_claims_validation_no_publish_or_subscribe() {
110		let claims = Claims {
111			root: "test-path".to_string(),
112			publish: vec![],
113			subscribe: vec![],
114			cluster: false,
115			expires: None,
116			issued: None,
117		};
118
119		let result = claims.validate();
120		assert!(result.is_err());
121		assert!(
122			result
123				.unwrap_err()
124				.to_string()
125				.contains("no publish or subscribe allowed; token is useless")
126		);
127	}
128
129	#[test]
130	fn test_claims_validation_only_publish() {
131		let claims = Claims {
132			root: "test-path".to_string(),
133			publish: vec!["test-pub".into()],
134			subscribe: vec![],
135			cluster: false,
136			expires: None,
137			issued: None,
138		};
139
140		assert!(claims.validate().is_ok());
141	}
142
143	#[test]
144	fn test_claims_validation_only_subscribe() {
145		let claims = Claims {
146			root: "test-path".to_string(),
147			publish: vec![],
148			subscribe: vec!["test-sub".into()],
149			cluster: false,
150			expires: None,
151			issued: None,
152		};
153
154		assert!(claims.validate().is_ok());
155	}
156
157	#[test]
158	fn test_claims_validation_path_not_prefix_relative_publish() {
159		let claims = Claims {
160			root: "test-path".to_string(),        // no trailing slash
161			publish: vec!["relative-pub".into()], // relative path without leading slash
162			subscribe: vec![],
163			cluster: false,
164			expires: None,
165			issued: None,
166		};
167
168		let result = claims.validate();
169		assert!(result.is_ok()); // Now passes because slashes are implicitly added
170	}
171
172	#[test]
173	fn test_claims_validation_path_not_prefix_relative_subscribe() {
174		let claims = Claims {
175			root: "test-path".to_string(), // no trailing slash
176			publish: vec![],
177			subscribe: vec!["relative-sub".into()], // relative path without leading slash
178			cluster: false,
179			expires: None,
180			issued: None,
181		};
182
183		let result = claims.validate();
184		assert!(result.is_ok()); // Now passes because slashes are implicitly added
185	}
186
187	#[test]
188	fn test_claims_validation_path_not_prefix_absolute_publish() {
189		let claims = Claims {
190			root: "test-path".to_string(),         // no trailing slash
191			publish: vec!["/absolute-pub".into()], // absolute path with leading slash
192			subscribe: vec![],
193			cluster: false,
194			expires: None,
195			issued: None,
196		};
197
198		assert!(claims.validate().is_ok());
199	}
200
201	#[test]
202	fn test_claims_validation_path_not_prefix_absolute_subscribe() {
203		let claims = Claims {
204			root: "test-path".to_string(), // no trailing slash
205			publish: vec![],
206			subscribe: vec!["/absolute-sub".into()], // absolute path with leading slash
207			cluster: false,
208			expires: None,
209			issued: None,
210		};
211
212		assert!(claims.validate().is_ok());
213	}
214
215	#[test]
216	fn test_claims_validation_path_not_prefix_empty_publish() {
217		let claims = Claims {
218			root: "test-path".to_string(), // no trailing slash
219			publish: vec!["".into()],      // empty string
220			subscribe: vec![],
221			cluster: false,
222			expires: None,
223			issued: None,
224		};
225
226		assert!(claims.validate().is_ok());
227	}
228
229	#[test]
230	fn test_claims_validation_path_not_prefix_empty_subscribe() {
231		let claims = Claims {
232			root: "test-path".to_string(), // no trailing slash
233			publish: vec![],
234			subscribe: vec!["".into()], // empty string
235			cluster: false,
236			expires: None,
237			issued: None,
238		};
239
240		assert!(claims.validate().is_ok());
241	}
242
243	#[test]
244	fn test_claims_validation_path_is_prefix() {
245		let claims = Claims {
246			root: "test-path".to_string(),          // with trailing slash
247			publish: vec!["relative-pub".into()],   // relative path is ok when path is prefix
248			subscribe: vec!["relative-sub".into()], // relative path is ok when path is prefix
249			cluster: false,
250			expires: None,
251			issued: None,
252		};
253
254		assert!(claims.validate().is_ok());
255	}
256
257	#[test]
258	fn test_claims_validation_empty_path() {
259		let claims = Claims {
260			root: "".to_string(), // empty path
261			publish: vec!["test-pub".into()],
262			subscribe: vec![],
263			cluster: false,
264			expires: None,
265			issued: None,
266		};
267
268		assert!(claims.validate().is_ok());
269	}
270
271	#[test]
272	fn test_claims_serde() {
273		let claims = create_test_claims();
274		let json = serde_json::to_string(&claims).unwrap();
275		let deserialized: Claims = serde_json::from_str(&json).unwrap();
276
277		assert_eq!(deserialized.root, claims.root);
278		assert_eq!(deserialized.publish, claims.publish);
279		assert_eq!(deserialized.subscribe, claims.subscribe);
280		assert_eq!(deserialized.cluster, claims.cluster);
281	}
282
283	#[test]
284	fn test_claims_default() {
285		let claims = Claims::default();
286		assert_eq!(claims.root, "");
287		assert!(claims.publish.is_empty());
288		assert!(claims.subscribe.is_empty());
289		assert!(!claims.cluster);
290		assert_eq!(claims.expires, None);
291		assert_eq!(claims.issued, None);
292	}
293
294	#[test]
295	fn test_is_false_helper() {
296		assert!(is_false(&false));
297		assert!(!is_false(&true));
298	}
299
300	#[test]
301	fn test_deserialize_string_as_vec() {
302		let json = r#"{
303			"root": "test",
304			"put": "single-publish",
305			"get": "single-subscribe"
306		}"#;
307
308		let claims: Claims = serde_json::from_str(json).unwrap();
309		assert_eq!(claims.publish, vec!["single-publish"]);
310		assert_eq!(claims.subscribe, vec!["single-subscribe"]);
311	}
312
313	#[test]
314	fn test_deserialize_vec_as_vec() {
315		let json = r#"{
316			"root": "test",
317			"put": ["pub1", "pub2"],
318			"get": ["sub1", "sub2"]
319		}"#;
320
321		let claims: Claims = serde_json::from_str(json).unwrap();
322		assert_eq!(claims.publish, vec!["pub1", "pub2"]);
323		assert_eq!(claims.subscribe, vec!["sub1", "sub2"]);
324	}
325
326	#[test]
327	fn test_deserialize_mixed() {
328		let json = r#"{
329			"root": "test",
330			"put": "single",
331			"get": ["multi1", "multi2"]
332		}"#;
333
334		let claims: Claims = serde_json::from_str(json).unwrap();
335		assert_eq!(claims.publish, vec!["single"]);
336		assert_eq!(claims.subscribe, vec!["multi1", "multi2"]);
337	}
338}