use serde::{Deserialize, Serialize};
use serde_with::{DurationSecondsWithFrac, TimestampSeconds, serde_as};
use std::net::SocketAddr;
use std::time::{Duration, SystemTime};
use crate::lease::Reason;
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct Request {
pub id: String,
#[serde(flatten)]
pub event: Event,
pub node: String,
pub transport: Transport,
pub remote: Option<SocketAddr>,
pub local: Option<SocketAddr>,
pub server_name: Option<String>,
pub alpn: Option<String>,
pub path: String,
pub query: Option<String>,
pub role: Option<Role>,
pub tls: Option<Peer>,
}
impl Request {
pub fn new(node: impl Into<String>, transport: Transport, path: impl Into<String>) -> Self {
let mut bytes = [0u8; 16];
aws_lc_rs::rand::fill(&mut bytes).expect("failed to generate a session id");
let id: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
Self {
id,
event: Event::Connect,
node: node.into(),
transport,
remote: None,
local: None,
server_name: None,
alpn: None,
path: path.into(),
query: None,
role: None,
tls: None,
}
}
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "event", rename_all = "lowercase")]
pub enum Event {
Connect,
Revalidate,
End {
reason: Reason,
#[serde_as(as = "DurationSecondsWithFrac<f64>")]
duration: Duration,
bytes: Bytes,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Transport {
Quic,
Iroh,
WebSocket,
Tcp,
Unix,
Http,
}
impl Transport {
pub const fn as_str(self) -> &'static str {
match self {
Self::Quic => "quic",
Self::Iroh => "iroh",
Self::WebSocket => "websocket",
Self::Tcp => "tcp",
Self::Unix => "unix",
Self::Http => "http",
}
}
}
impl std::fmt::Display for Transport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Publisher,
Subscriber,
}
#[serde_as]
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Peer {
pub name: String,
pub fingerprint: String,
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
pub expires: Option<SystemTime>,
pub issuer: String,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Bytes {
pub sent: u64,
pub received: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn request() -> Request {
let mut request = Request::new("relay-1", Transport::Quic, "/demo/room");
request.id = "00ff".into();
request.remote = Some("203.0.113.9:4433".parse().unwrap());
request.local = Some("[::1]:443".parse().unwrap());
request.server_name = Some("relay.example".into());
request.alpn = Some("moq-lite-05".into());
request.query = Some("jwt=abc".into());
request.role = Some(Role::Publisher);
request.tls = Some(Peer {
name: "edge0".into(),
fingerprint: "ab".repeat(32),
expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
issuer: "CN=cluster".into(),
});
request
}
#[test]
fn connect_round_trips_flat() {
let request = request();
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["event"], "connect");
assert_eq!(json["transport"], "quic");
assert_eq!(json["role"], "publisher");
assert_eq!(json["remote"], "203.0.113.9:4433");
assert_eq!(json["tls"]["expires"], 4_102_444_800_i64);
assert!(json.get("reason").is_none());
assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
}
#[test]
fn end_carries_its_facts_beside_the_rest() {
let mut request = request();
request.event = Event::End {
reason: Reason::Session("disconnected".into()),
duration: Duration::from_millis(1500),
bytes: Bytes { sent: 10, received: 20 },
};
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["event"], "end");
assert_eq!(json["reason"], "disconnected");
assert_eq!(json["duration"], 1.5);
assert_eq!(json["bytes"]["sent"], 10);
assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
}
#[test]
fn end_serializes_to_the_cross_language_vector() {
let mut request = Request::new("relay-1", Transport::WebSocket, "/demo/room");
request.id = "00ff".into();
request.remote = Some("203.0.113.9:4433".parse().unwrap());
request.query = Some("jwt=abc".into());
request.event = Event::End {
reason: Reason::Expired,
duration: Duration::from_millis(1500),
bytes: Bytes { sent: 10, received: 20 },
};
assert_eq!(
serde_json::to_string(&request).unwrap(),
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"}"#
);
request.event = Event::End {
reason: Reason::Invalid,
duration: Duration::from_millis(1500),
bytes: Bytes { sent: 10, received: 20 },
};
assert_eq!(
serde_json::to_string(&request).unwrap(),
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"}"#
);
}
#[test]
fn a_unix_session_has_no_addresses() {
let request = Request::new("relay-1", Transport::Unix, "");
let json = serde_json::to_value(&request).unwrap();
assert!(json.get("remote").is_none());
assert_eq!(json["transport"], "unix");
assert_eq!(serde_json::from_value::<Request>(json).unwrap(), request);
}
#[test]
fn new_mints_a_128_bit_hex_id() {
let a = Request::new("relay-1", Transport::Quic, "/");
let b = Request::new("relay-1", Transport::Quic, "/");
assert_eq!(a.id.len(), 32);
assert!(a.id.chars().all(|c| c.is_ascii_hexdigit()), "{}", a.id);
assert_ne!(a.id, b.id);
}
}