1use serde::{Deserialize, Serialize};
2use serde_with::{DurationSecondsWithFrac, TimestampSeconds, serde_as};
3use std::net::SocketAddr;
4use std::time::{Duration, SystemTime};
5
6use crate::lease::Reason;
7
8#[serde_with::skip_serializing_none]
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16#[non_exhaustive]
17pub struct Request {
18 pub id: String,
20
21 #[serde(flatten)]
23 pub event: Event,
24
25 pub node: String,
27
28 pub transport: Transport,
30
31 pub remote: Option<SocketAddr>,
33
34 pub local: Option<SocketAddr>,
36
37 pub server_name: Option<String>,
39
40 pub alpn: Option<String>,
42
43 pub path: String,
45
46 pub query: Option<String>,
48
49 pub role: Option<Role>,
51
52 pub tls: Option<Peer>,
54}
55
56impl Request {
57 pub fn new(node: impl Into<String>, transport: Transport, path: impl Into<String>) -> Self {
62 let mut bytes = [0u8; 16];
63 aws_lc_rs::rand::fill(&mut bytes).expect("failed to generate a session id");
64 let id: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
65 Self {
66 id,
67 event: Event::Connect,
68 node: node.into(),
69 transport,
70 remote: None,
71 local: None,
72 server_name: None,
73 alpn: None,
74 path: path.into(),
75 query: None,
76 role: None,
77 tls: None,
78 }
79 }
80}
81
82#[serde_as]
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85#[serde(tag = "event", rename_all = "lowercase")]
86pub enum Event {
87 Connect,
89 Revalidate,
91 End {
93 reason: Reason,
95 #[serde_as(as = "DurationSecondsWithFrac<f64>")]
97 duration: Duration,
98 bytes: Bytes,
100 },
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum Transport {
108 Quic,
110 Iroh,
112 WebSocket,
114 Tcp,
116 Unix,
118 Http,
121}
122
123impl Transport {
124 pub const fn as_str(self) -> &'static str {
126 match self {
127 Self::Quic => "quic",
128 Self::Iroh => "iroh",
129 Self::WebSocket => "websocket",
130 Self::Tcp => "tcp",
131 Self::Unix => "unix",
132 Self::Http => "http",
133 }
134 }
135}
136
137impl std::fmt::Display for Transport {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.write_str(self.as_str())
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(rename_all = "lowercase")]
146pub enum Role {
147 Publisher,
149 Subscriber,
151}
152
153#[serde_as]
156#[serde_with::skip_serializing_none]
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
158pub struct Peer {
159 pub name: String,
164 pub fingerprint: String,
166 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
168 pub expires: Option<SystemTime>,
169 pub issuer: String,
171}
172
173#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
175pub struct Bytes {
176 pub sent: u64,
178 pub received: u64,
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn request() -> Request {
187 let mut request = Request::new("relay-1", Transport::Quic, "/demo/room");
188 request.id = "00ff".into();
189 request.remote = Some("203.0.113.9:4433".parse().unwrap());
190 request.local = Some("[::1]:443".parse().unwrap());
191 request.server_name = Some("relay.example".into());
192 request.alpn = Some("moq-lite-05".into());
193 request.query = Some("jwt=abc".into());
194 request.role = Some(Role::Publisher);
195 request.tls = Some(Peer {
196 name: "edge0".into(),
197 fingerprint: "ab".repeat(32),
198 expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
199 issuer: "CN=cluster".into(),
200 });
201 request
202 }
203
204 #[test]
205 fn connect_round_trips_flat() {
206 let request = request();
207 let json = serde_json::to_value(&request).unwrap();
208 assert_eq!(json["event"], "connect");
209 assert_eq!(json["transport"], "quic");
210 assert_eq!(json["role"], "publisher");
211 assert_eq!(json["remote"], "203.0.113.9:4433");
212 assert_eq!(json["tls"]["expires"], 4_102_444_800_i64);
213 assert!(json.get("reason").is_none());
214 assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
215 }
216
217 #[test]
218 fn end_carries_its_facts_beside_the_rest() {
219 let mut request = request();
220 request.event = Event::End {
221 reason: Reason::Session("disconnected".into()),
222 duration: Duration::from_millis(1500),
223 bytes: Bytes { sent: 10, received: 20 },
224 };
225 let json = serde_json::to_value(&request).unwrap();
226 assert_eq!(json["event"], "end");
227 assert_eq!(json["reason"], "disconnected");
228 assert_eq!(json["duration"], 1.5);
229 assert_eq!(json["bytes"]["sent"], 10);
230 assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
231 }
232
233 #[test]
236 fn end_serializes_to_the_cross_language_vector() {
237 let mut request = Request::new("relay-1", Transport::WebSocket, "/demo/room");
238 request.id = "00ff".into();
239 request.remote = Some("203.0.113.9:4433".parse().unwrap());
240 request.query = Some("jwt=abc".into());
241 request.event = Event::End {
242 reason: Reason::Expired,
243 duration: Duration::from_millis(1500),
244 bytes: Bytes { sent: 10, received: 20 },
245 };
246 assert_eq!(
247 serde_json::to_string(&request).unwrap(),
248 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"}"#
249 );
250
251 request.event = Event::End {
252 reason: Reason::Invalid,
253 duration: Duration::from_millis(1500),
254 bytes: Bytes { sent: 10, received: 20 },
255 };
256 assert_eq!(
257 serde_json::to_string(&request).unwrap(),
258 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"}"#
259 );
260 }
261
262 #[test]
263 fn a_unix_session_has_no_addresses() {
264 let request = Request::new("relay-1", Transport::Unix, "");
265 let json = serde_json::to_value(&request).unwrap();
266 assert!(json.get("remote").is_none());
267 assert_eq!(json["transport"], "unix");
268 assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
269 }
270
271 #[test]
272 fn new_mints_a_128_bit_hex_id() {
273 let a = Request::new("relay-1", Transport::Quic, "/");
274 let b = Request::new("relay-1", Transport::Quic, "/");
275 assert_eq!(a.id.len(), 32);
276 assert!(a.id.chars().all(|c| c.is_ascii_hexdigit()), "{}", a.id);
277 assert_ne!(a.id, b.id);
278 }
279}