ap_proxy_protocol/rendezvous.rs
1//! Rendezvous codes for temporary peer discovery.
2//!
3//! The rendezvous system provides short, human-readable codes that clients can share
4//! to discover each other's identities without exchanging long-lived public keys.
5//!
6//! # Overview
7//! 1. Client requests a rendezvous code from the server
8//! 2. Server generates a unique code (e.g., "ABC-DEF-GHI") and maps it to the client's identity
9//! 3. Peer uses the code to look up the client's identity
10//! 4. Server returns the identity and deletes the code (single-use)
11
12use rand::distributions::{Alphanumeric, DistString};
13use serde::{Deserialize, Serialize};
14
15/// A temporary rendezvous code for peer discovery.
16///
17/// Rendezvous codes are short, human-readable identifiers (format: "ABC-DEF-GHI") that
18/// temporarily map to a client's identity on the proxy server.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RendezvousCode {
21 code: String,
22}
23
24impl Default for RendezvousCode {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl RendezvousCode {
31 /// Generate a new random rendezvous code.
32 ///
33 /// Creates a 9-character code from the alphanumeric alphabet (A-Z, 0-9),
34 /// formatted with hyphen separators (e.g., "ABC-DEF-GHI").
35 ///
36 /// # Entropy
37 ///
38 /// With an alphabet of 36 characters and 9 positions:
39 /// - Total possibilities: 36^9 = 101,559,956,668,416
40 /// - Given the 5-minute lifetime, brute-force is impractical
41 ///
42 /// # Examples
43 ///
44 /// ```
45 /// use ap_proxy_protocol::RendezvousCode;
46 ///
47 /// let code1 = RendezvousCode::new();
48 /// let code2 = RendezvousCode::new();
49 ///
50 /// // Each call generates a different random code
51 /// assert_ne!(code1.as_str(), code2.as_str());
52 ///
53 /// // Format is always ABC-DEF-GHI style
54 /// assert_eq!(code1.as_str().len(), 11); // 9 chars + 2 hyphens
55 /// assert_eq!(code1.as_str().chars().nth(3), Some('-'));
56 /// assert_eq!(code1.as_str().chars().nth(7), Some('-'));
57 /// ```
58 pub fn new() -> Self {
59 let mut rng = rand::thread_rng();
60
61 // The code has an alphabet of size 36. With 9 characters, that's
62 // 36^9 = 101,559,956,668,416 possible codes. The codes are short-lived, and the connections to the proxy are rate-limited,
63 // which is why this is considered sufficient.
64 let code = Alphanumeric.sample_string(&mut rng, 9);
65 let code = code.to_ascii_uppercase();
66 // SAFETY: Alphanumeric + to_ascii_uppercase produces only ASCII characters,
67 // so indexing at character boundaries is safe.
68 #[allow(clippy::string_slice)]
69 let code = format!("{}-{}-{}", &code[..3], &code[3..6], &code[6..]);
70
71 RendezvousCode { code }
72 }
73
74 /// Create a rendezvous code from an existing string.
75 ///
76 /// Useful for:
77 /// - Testing with known codes
78 /// - Parsing user input
79 /// - Deserializing from storage
80 ///
81 /// No validation is performed - the caller is responsible for ensuring
82 /// the code is in the correct format.
83 ///
84 /// # Examples
85 ///
86 /// ```
87 /// use ap_proxy_protocol::RendezvousCode;
88 ///
89 /// let code = RendezvousCode::from_string("ABC-DEF-GHI".to_string());
90 /// assert_eq!(code.as_str(), "ABC-DEF-GHI");
91 /// ```
92 pub fn from_string(code: String) -> Self {
93 RendezvousCode { code }
94 }
95
96 /// Get the code string.
97 ///
98 /// Returns the formatted code (e.g., "ABC-DEF-GHI") that can be displayed to
99 /// users or sent to the server.
100 ///
101 /// # Examples
102 ///
103 /// ```
104 /// use ap_proxy_protocol::RendezvousCode;
105 ///
106 /// let code = RendezvousCode::new();
107 /// println!("Your code: {}", code.as_str());
108 /// ```
109 pub fn as_str(&self) -> &str {
110 &self.code
111 }
112}
113
114impl std::fmt::Display for RendezvousCode {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}", self.code)
117 }
118}