1use serde::{Deserialize, Serialize};
2use serde_with::base64::{Base64, UrlSafe};
3use serde_with::formats::Unpadded;
4use serde_with::{DurationSecondsWithFrac, TimestampSeconds, serde_as};
5use std::net::SocketAddr;
6use std::time::{Duration, SystemTime};
7
8use crate::lease::Reason;
9
10#[serde_with::skip_serializing_none]
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18#[non_exhaustive]
19pub struct Request {
20 pub id: String,
22
23 #[serde(flatten)]
25 pub event: Event,
26
27 pub node: String,
29
30 pub transport: Transport,
32
33 pub remote: Option<SocketAddr>,
35
36 pub local: Option<SocketAddr>,
38
39 pub server_name: Option<String>,
41
42 pub alpn: Option<String>,
44
45 pub path: String,
47
48 pub query: Option<String>,
50
51 pub token: Option<Token>,
53
54 pub role: Option<Role>,
56
57 pub tls: Option<Peer>,
59}
60
61impl Request {
62 pub fn new(node: impl Into<String>, transport: Transport, path: impl Into<String>) -> Self {
67 let mut bytes = [0u8; 16];
68 aws_lc_rs::rand::fill(&mut bytes).expect("failed to generate a session id");
69 let id: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
70 Self {
71 id,
72 event: Event::Connect,
73 node: node.into(),
74 transport,
75 remote: None,
76 local: None,
77 server_name: None,
78 alpn: None,
79 path: path.into(),
80 query: None,
81 token: None,
82 role: None,
83 tls: None,
84 }
85 }
86}
87
88#[serde_as]
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct Token {
92 pub kind: u64,
94 #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
96 pub value: Vec<u8>,
97}
98
99impl Token {
100 pub const OUT_OF_BAND: u64 = 0x0;
102 pub const CAT: u64 = 0x1;
104}
105
106#[serde_as]
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109#[serde(tag = "event", rename_all = "lowercase")]
110pub enum Event {
111 Connect,
113 Revalidate,
115 End {
117 reason: Reason,
119 #[serde_as(as = "DurationSecondsWithFrac<f64>")]
121 duration: Duration,
122 bytes: Bytes,
124 },
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
130#[serde(rename_all = "lowercase")]
131pub enum Transport {
132 Quic,
134 Iroh,
136 WebSocket,
138 Tcp,
140 Unix,
142 Http,
145}
146
147impl Transport {
148 pub const fn as_str(self) -> &'static str {
150 match self {
151 Self::Quic => "quic",
152 Self::Iroh => "iroh",
153 Self::WebSocket => "websocket",
154 Self::Tcp => "tcp",
155 Self::Unix => "unix",
156 Self::Http => "http",
157 }
158 }
159}
160
161impl std::fmt::Display for Transport {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.write_str(self.as_str())
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
169#[serde(rename_all = "lowercase")]
170pub enum Role {
171 Publisher,
173 Subscriber,
175}
176
177#[serde_as]
180#[serde_with::skip_serializing_none]
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
182pub struct Peer {
183 pub name: String,
188 pub fingerprint: String,
190 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
192 pub expires: Option<SystemTime>,
193 pub issuer: String,
195}
196
197#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
199pub struct Bytes {
200 pub sent: u64,
202 pub received: u64,
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn request() -> Request {
211 let mut request = Request::new("relay-1", Transport::Quic, "/demo/room");
212 request.id = "00ff".into();
213 request.remote = Some("203.0.113.9:4433".parse().unwrap());
214 request.local = Some("[::1]:443".parse().unwrap());
215 request.server_name = Some("relay.example".into());
216 request.alpn = Some("moq-lite-05".into());
217 request.query = Some("jwt=abc".into());
218 request.role = Some(Role::Publisher);
219 request.tls = Some(Peer {
220 name: "edge0".into(),
221 fingerprint: "ab".repeat(32),
222 expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
223 issuer: "CN=cluster".into(),
224 });
225 request
226 }
227
228 #[test]
229 fn connect_round_trips_flat() {
230 let request = request();
231 let json = serde_json::to_value(&request).unwrap();
232 assert_eq!(json["event"], "connect");
233 assert_eq!(json["transport"], "quic");
234 assert_eq!(json["role"], "publisher");
235 assert_eq!(json["remote"], "203.0.113.9:4433");
236 assert_eq!(json["tls"]["expires"], 4_102_444_800_i64);
237 assert!(json.get("reason").is_none());
238 assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
239 }
240
241 #[test]
242 fn end_carries_its_facts_beside_the_rest() {
243 let mut request = request();
244 request.event = Event::End {
245 reason: Reason::Session("disconnected".into()),
246 duration: Duration::from_millis(1500),
247 bytes: Bytes { sent: 10, received: 20 },
248 };
249 let json = serde_json::to_value(&request).unwrap();
250 assert_eq!(json["event"], "end");
251 assert_eq!(json["reason"], "disconnected");
252 assert_eq!(json["duration"], 1.5);
253 assert_eq!(json["bytes"]["sent"], 10);
254 assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
255 }
256
257 #[test]
260 fn end_serializes_to_the_cross_language_vector() {
261 let mut request = Request::new("relay-1", Transport::WebSocket, "/demo/room");
262 request.id = "00ff".into();
263 request.remote = Some("203.0.113.9:4433".parse().unwrap());
264 request.query = Some("jwt=abc".into());
265 request.event = Event::End {
266 reason: Reason::Expired,
267 duration: Duration::from_millis(1500),
268 bytes: Bytes { sent: 10, received: 20 },
269 };
270 assert_eq!(
271 serde_json::to_string(&request).unwrap(),
272 r#"{"id":"00ff","event":"end","reason":"expired","duration":1.5,"bytes":{"sent":10,"received":20},"node":"relay-1","transport":"websocket","remote":"203.0.113.9:4433","path":"/demo/room","query":"jwt=abc"}"#
273 );
274
275 request.event = Event::End {
276 reason: Reason::Invalid,
277 duration: Duration::from_millis(1500),
278 bytes: Bytes { sent: 10, received: 20 },
279 };
280 assert_eq!(
281 serde_json::to_string(&request).unwrap(),
282 r#"{"id":"00ff","event":"end","reason":"invalid","duration":1.5,"bytes":{"sent":10,"received":20},"node":"relay-1","transport":"websocket","remote":"203.0.113.9:4433","path":"/demo/room","query":"jwt=abc"}"#
283 );
284 }
285
286 #[test]
289 fn a_setup_token_serializes_as_base64url() {
290 let mut request = Request::new("relay-1", Transport::Quic, "/demo/room");
291 request.id = "00ff".into();
292 request.token = Some(Token {
293 kind: Token::CAT,
294 value: vec![0x00, 0xfb, 0xff],
295 });
296 let json = serde_json::to_string(&request).unwrap();
297 assert_eq!(
298 json,
299 r#"{"id":"00ff","event":"connect","node":"relay-1","transport":"quic","path":"/demo/room","token":{"kind":1,"value":"APv_"}}"#
300 );
301 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
302
303 for value in ["A", "AB", "APv_A", "AP+/"] {
305 let json = format!(r#"{{"kind":0,"value":"{value}"}}"#);
306 assert!(serde_json::from_str::<Token>(&json).is_err(), "{value}");
307 }
308 for value in ["", "AA", "AAA", "AAAA", "AQ", "AAE"] {
309 let json = format!(r#"{{"kind":0,"value":"{value}"}}"#);
310 assert!(serde_json::from_str::<Token>(&json).is_ok(), "{value}");
311 }
312 }
313
314 #[test]
315 fn a_unix_session_has_no_addresses() {
316 let request = Request::new("relay-1", Transport::Unix, "");
317 let json = serde_json::to_value(&request).unwrap();
318 assert!(json.get("remote").is_none());
319 assert_eq!(json["transport"], "unix");
320 assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
321 }
322
323 #[test]
324 fn new_mints_a_128_bit_hex_id() {
325 let a = Request::new("relay-1", Transport::Quic, "/");
326 let b = Request::new("relay-1", Transport::Quic, "/");
327 assert_eq!(a.id.len(), 32);
328 assert!(a.id.chars().all(|c| c.is_ascii_hexdigit()), "{}", a.id);
329 assert_ne!(a.id, b.id);
330 }
331}