hashtree_cli/webrtc/
types.rs

1//! WebRTC signaling types compatible with iris-client and hashtree-ts
2
3use rand::Rng;
4use serde::{Deserialize, Serialize};
5
6// HTL (Hops To Live) constants - Freenet-style probabilistic decrement
7pub const MAX_HTL: u8 = 10;
8pub const DECREMENT_AT_MAX_PROB: f64 = 0.5;  // 50% chance to decrement at max
9pub const DECREMENT_AT_MIN_PROB: f64 = 0.25; // 25% chance to decrement at 1
10
11/// Per-peer HTL decrement configuration (Freenet-style)
12/// Stored per peer connection to prevent probing attacks
13#[derive(Debug, Clone)]
14pub struct PeerHTLConfig {
15    pub decrement_at_max: bool,  // Whether to decrement when HTL is at max
16    pub decrement_at_min: bool,  // Whether to decrement when HTL is 1
17}
18
19impl PeerHTLConfig {
20    /// Generate random HTL decrement config for a new peer connection
21    /// This is decided once per peer, not per request, to prevent probing
22    pub fn new() -> Self {
23        let mut rng = rand::thread_rng();
24        Self {
25            decrement_at_max: rng.gen_bool(DECREMENT_AT_MAX_PROB),
26            decrement_at_min: rng.gen_bool(DECREMENT_AT_MIN_PROB),
27        }
28    }
29}
30
31impl Default for PeerHTLConfig {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37/// Decrement HTL using Freenet-style probabilistic rules
38/// - At max HTL: probabilistic decrement (50% by default)
39/// - At HTL=1: probabilistic decrement (25% by default)
40/// - Otherwise: always decrement
41///
42/// Returns new HTL value
43pub fn decrement_htl(htl: u8, config: &PeerHTLConfig) -> u8 {
44    // Clamp to max
45    let htl = htl.min(MAX_HTL);
46
47    // Already dead
48    if htl == 0 {
49        return 0;
50    }
51
52    // At max: probabilistic decrement
53    if htl == MAX_HTL {
54        return if config.decrement_at_max { htl - 1 } else { htl };
55    }
56
57    // At min (1): probabilistic decrement
58    if htl == 1 {
59        return if config.decrement_at_min { 0 } else { htl };
60    }
61
62    // Middle: always decrement
63    htl - 1
64}
65
66/// Check if a request should be forwarded based on HTL
67pub fn should_forward(htl: u8) -> bool {
68    htl > 0
69}
70
71/// Event kind for WebRTC signaling (ephemeral kind 25050)
72/// All signaling uses this kind - hellos use #l tag, directed use gift wrap
73pub const WEBRTC_KIND: u64 = 25050;
74
75/// Tag for hello messages (broadcast discovery)
76pub const HELLO_TAG: &str = "hello";
77
78/// Legacy tag for WebRTC signaling messages (kept for compatibility)
79pub const WEBRTC_TAG: &str = "webrtc";
80
81/// Generate a UUID for peer identification
82pub fn generate_uuid() -> String {
83    use rand::Rng;
84    let mut rng = rand::thread_rng();
85    format!(
86        "{}{}",
87        (0..15).map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap()).collect::<String>(),
88        (0..15).map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap()).collect::<String>()
89    )
90}
91
92/// Peer identifier combining pubkey and session UUID
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub struct PeerId {
95    pub pubkey: String,
96    pub uuid: String,
97}
98
99impl PeerId {
100    pub fn new(pubkey: String, uuid: Option<String>) -> Self {
101        Self {
102            pubkey,
103            uuid: uuid.unwrap_or_else(generate_uuid),
104        }
105    }
106
107    pub fn from_string(s: &str) -> Option<Self> {
108        let parts: Vec<&str> = s.split(':').collect();
109        if parts.len() == 2 {
110            Some(Self {
111                pubkey: parts[0].to_string(),
112                uuid: parts[1].to_string(),
113            })
114        } else {
115            None
116        }
117    }
118
119    pub fn short(&self) -> String {
120        format!("{}:{}", &self.pubkey[..8.min(self.pubkey.len())], &self.uuid[..6.min(self.uuid.len())])
121    }
122}
123
124impl std::fmt::Display for PeerId {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "{}:{}", self.pubkey, self.uuid)
127    }
128}
129
130/// Hello message for peer discovery
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct HelloMessage {
133    #[serde(rename = "type")]
134    pub msg_type: String,
135    #[serde(rename = "peerId")]
136    pub peer_id: String,
137}
138
139/// WebRTC offer message
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct OfferMessage {
142    #[serde(rename = "type")]
143    pub msg_type: String,
144    pub offer: serde_json::Value,
145    pub recipient: String,
146    #[serde(rename = "peerId")]
147    pub peer_id: String,
148}
149
150/// WebRTC answer message
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AnswerMessage {
153    #[serde(rename = "type")]
154    pub msg_type: String,
155    pub answer: serde_json::Value,
156    pub recipient: String,
157    #[serde(rename = "peerId")]
158    pub peer_id: String,
159}
160
161/// ICE candidate message
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct CandidateMessage {
164    #[serde(rename = "type")]
165    pub msg_type: String,
166    pub candidate: serde_json::Value,
167    pub recipient: String,
168    #[serde(rename = "peerId")]
169    pub peer_id: String,
170}
171
172/// Batched ICE candidates message (hashtree-ts extension)
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct CandidatesMessage {
175    #[serde(rename = "type")]
176    pub msg_type: String,
177    pub candidates: Vec<serde_json::Value>,
178    pub recipient: String,
179    #[serde(rename = "peerId")]
180    pub peer_id: String,
181}
182
183/// All signaling message types
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(tag = "type")]
186pub enum SignalingMessage {
187    #[serde(rename = "hello")]
188    Hello { #[serde(rename = "peerId")] peer_id: String },
189    #[serde(rename = "offer")]
190    Offer {
191        offer: serde_json::Value,
192        recipient: String,
193        #[serde(rename = "peerId")]
194        peer_id: String,
195    },
196    #[serde(rename = "answer")]
197    Answer {
198        answer: serde_json::Value,
199        recipient: String,
200        #[serde(rename = "peerId")]
201        peer_id: String,
202    },
203    #[serde(rename = "candidate")]
204    Candidate {
205        candidate: serde_json::Value,
206        recipient: String,
207        #[serde(rename = "peerId")]
208        peer_id: String,
209    },
210    #[serde(rename = "candidates")]
211    Candidates {
212        candidates: Vec<serde_json::Value>,
213        recipient: String,
214        #[serde(rename = "peerId")]
215        peer_id: String,
216    },
217}
218
219impl SignalingMessage {
220    pub fn msg_type(&self) -> &str {
221        match self {
222            SignalingMessage::Hello { .. } => "hello",
223            SignalingMessage::Offer { .. } => "offer",
224            SignalingMessage::Answer { .. } => "answer",
225            SignalingMessage::Candidate { .. } => "candidate",
226            SignalingMessage::Candidates { .. } => "candidates",
227        }
228    }
229
230    pub fn recipient(&self) -> Option<&str> {
231        match self {
232            SignalingMessage::Hello { .. } => None,
233            SignalingMessage::Offer { recipient, .. } => Some(recipient),
234            SignalingMessage::Answer { recipient, .. } => Some(recipient),
235            SignalingMessage::Candidate { recipient, .. } => Some(recipient),
236            SignalingMessage::Candidates { recipient, .. } => Some(recipient),
237        }
238    }
239
240    pub fn peer_id(&self) -> &str {
241        match self {
242            SignalingMessage::Hello { peer_id } => peer_id,
243            SignalingMessage::Offer { peer_id, .. } => peer_id,
244            SignalingMessage::Answer { peer_id, .. } => peer_id,
245            SignalingMessage::Candidate { peer_id, .. } => peer_id,
246            SignalingMessage::Candidates { peer_id, .. } => peer_id,
247        }
248    }
249
250    pub fn hello(peer_id: &str) -> Self {
251        SignalingMessage::Hello {
252            peer_id: peer_id.to_string(),
253        }
254    }
255
256    pub fn offer(offer: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
257        SignalingMessage::Offer {
258            offer,
259            recipient: recipient.to_string(),
260            peer_id: peer_id.to_string(),
261        }
262    }
263
264    pub fn answer(answer: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
265        SignalingMessage::Answer {
266            answer,
267            recipient: recipient.to_string(),
268            peer_id: peer_id.to_string(),
269        }
270    }
271
272    pub fn candidate(candidate: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
273        SignalingMessage::Candidate {
274            candidate,
275            recipient: recipient.to_string(),
276            peer_id: peer_id.to_string(),
277        }
278    }
279}
280
281/// Configuration for WebRTC manager
282#[derive(Clone)]
283pub struct WebRTCConfig {
284    /// Nostr relays for signaling
285    pub relays: Vec<String>,
286    /// Maximum outbound connections (legacy, use pools instead)
287    pub max_outbound: usize,
288    /// Maximum inbound connections (legacy, use pools instead)
289    pub max_inbound: usize,
290    /// Hello message interval in milliseconds
291    pub hello_interval_ms: u64,
292    /// Message timeout in milliseconds
293    pub message_timeout_ms: u64,
294    /// STUN servers for NAT traversal
295    pub stun_servers: Vec<String>,
296    /// Enable debug logging
297    pub debug: bool,
298    /// Pool settings for follows and other peers
299    pub pools: PoolSettings,
300}
301
302impl Default for WebRTCConfig {
303    fn default() -> Self {
304        Self {
305            relays: vec![
306                "wss://relay.damus.io".to_string(),
307                "wss://relay.primal.net".to_string(),
308                "wss://nos.lol".to_string(),
309                "wss://temp.iris.to".to_string(),
310                "wss://relay.snort.social".to_string(),
311            ],
312            max_outbound: 6,
313            max_inbound: 6,
314            hello_interval_ms: 10000,
315            message_timeout_ms: 15000,
316            stun_servers: vec![
317                "stun:stun.iris.to:3478".to_string(),
318                "stun:stun.l.google.com:19302".to_string(),
319                "stun:stun.cloudflare.com:3478".to_string(),
320            ],
321            debug: false,
322            pools: PoolSettings::default(),
323        }
324    }
325}
326
327/// Peer connection status
328#[derive(Debug, Clone)]
329pub struct PeerStatus {
330    pub peer_id: String,
331    pub pubkey: String,
332    pub state: String,
333    pub direction: PeerDirection,
334    pub connected_at: Option<std::time::Instant>,
335    pub pool: PeerPool,
336}
337
338/// Direction of peer connection
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum PeerDirection {
341    Inbound,
342    Outbound,
343}
344
345/// Peer state change event for signaling layer notification
346#[derive(Debug, Clone)]
347pub enum PeerStateEvent {
348    /// Peer connection succeeded
349    Connected(PeerId),
350    /// Peer connection failed
351    Failed(PeerId),
352    /// Peer disconnected
353    Disconnected(PeerId),
354}
355
356/// Pool type for peer classification
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
358pub enum PeerPool {
359    /// Users in social graph (followed or followers)
360    Follows,
361    /// Everyone else
362    Other,
363}
364
365/// Configuration for a peer pool
366#[derive(Debug, Clone, Copy)]
367pub struct PoolConfig {
368    /// Maximum connections in this pool
369    pub max_connections: usize,
370    /// Number of connections to consider "satisfied" (stop sending hellos)
371    pub satisfied_connections: usize,
372}
373
374impl Default for PoolConfig {
375    fn default() -> Self {
376        Self {
377            max_connections: 10,
378            satisfied_connections: 5,
379        }
380    }
381}
382
383/// Pool settings for both pools
384#[derive(Debug, Clone)]
385pub struct PoolSettings {
386    pub follows: PoolConfig,
387    pub other: PoolConfig,
388}
389
390impl Default for PoolSettings {
391    fn default() -> Self {
392        Self {
393            follows: PoolConfig {
394                max_connections: 20,
395                satisfied_connections: 10,
396            },
397            other: PoolConfig {
398                max_connections: 10,
399                satisfied_connections: 5,
400            },
401        }
402    }
403}
404
405impl std::fmt::Display for PeerDirection {
406    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        match self {
408            PeerDirection::Inbound => write!(f, "inbound"),
409            PeerDirection::Outbound => write!(f, "outbound"),
410        }
411    }
412}
413
414/// Message type bytes (prefix before MessagePack body)
415pub const MSG_TYPE_REQUEST: u8 = 0x00;
416pub const MSG_TYPE_RESPONSE: u8 = 0x01;
417
418/// Hashtree data channel protocol messages
419/// Shared between WebRTC data channels and WebSocket transport
420///
421/// Wire format: [type byte][msgpack body]
422/// Request:  [0x00][msgpack: {h: bytes32, htl?: u8}]
423/// Response: [0x01][msgpack: {h: bytes32, d: bytes}]
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct DataRequest {
427    #[serde(with = "serde_bytes")]
428    pub h: Vec<u8>,  // 32-byte hash
429    #[serde(default = "default_htl", skip_serializing_if = "is_max_htl")]
430    pub htl: u8,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct DataResponse {
435    #[serde(with = "serde_bytes")]
436    pub h: Vec<u8>,  // 32-byte hash
437    #[serde(with = "serde_bytes")]
438    pub d: Vec<u8>,  // Data
439}
440
441#[derive(Debug, Clone)]
442pub enum DataMessage {
443    Request(DataRequest),
444    Response(DataResponse),
445}
446
447fn default_htl() -> u8 {
448    MAX_HTL
449}
450
451fn is_max_htl(htl: &u8) -> bool {
452    *htl == MAX_HTL
453}
454
455/// Encode a request to wire format: [0x00][msgpack body]
456/// Uses named fields for cross-language compatibility with TypeScript
457pub fn encode_request(req: &DataRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
458    let body = rmp_serde::to_vec_named(req)?;
459    let mut result = Vec::with_capacity(1 + body.len());
460    result.push(MSG_TYPE_REQUEST);
461    result.extend(body);
462    Ok(result)
463}
464
465/// Encode a response to wire format: [0x01][msgpack body]
466/// Uses named fields for cross-language compatibility with TypeScript
467pub fn encode_response(res: &DataResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
468    let body = rmp_serde::to_vec_named(res)?;
469    let mut result = Vec::with_capacity(1 + body.len());
470    result.push(MSG_TYPE_RESPONSE);
471    result.extend(body);
472    Ok(result)
473}
474
475/// Parse a wire format message
476pub fn parse_message(data: &[u8]) -> Result<DataMessage, rmp_serde::decode::Error> {
477    if data.is_empty() {
478        return Err(rmp_serde::decode::Error::LengthMismatch(0));
479    }
480
481    let msg_type = data[0];
482    let body = &data[1..];
483
484    match msg_type {
485        MSG_TYPE_REQUEST => {
486            let req: DataRequest = rmp_serde::from_slice(body)?;
487            Ok(DataMessage::Request(req))
488        }
489        MSG_TYPE_RESPONSE => {
490            let res: DataResponse = rmp_serde::from_slice(body)?;
491            Ok(DataMessage::Response(res))
492        }
493        _ => Err(rmp_serde::decode::Error::LengthMismatch(msg_type as u32)),
494    }
495}
496
497/// Convert hash to hex string for logging/map keys
498pub fn hash_to_hex(hash: &[u8]) -> String {
499    hash.iter().map(|b| format!("{:02x}", b)).collect()
500}
501
502/// Encode a DataMessage to wire format (deprecated - use encode_request/encode_response)
503pub fn encode_message(msg: &DataMessage) -> Result<Vec<u8>, rmp_serde::encode::Error> {
504    match msg {
505        DataMessage::Request(req) => encode_request(req),
506        DataMessage::Response(res) => encode_response(res),
507    }
508}