Skip to main content

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