p2p/webrtc/signaling_method/
http.rs

1//! HTTP signaling transport configuration.
2//!
3//! This module defines the HTTP-specific signaling transport configuration
4//! for WebRTC connections in OpenMina's peer-to-peer network.
5//!
6//! ## HTTP Signaling
7//!
8//! HTTP signaling provides a simple, widely-supported transport method for
9//! WebRTC offer/answer exchange. It uses standard HTTP requests to POST
10//! WebRTC offers to signaling servers and receive answers in response.
11//!
12//! ## Transport Characteristics
13//!
14//! - **Request/Response Model**: Uses HTTP POST for offer delivery
15//! - **Stateless**: Each signaling exchange is independent
16//! - **Firewall Friendly**: Works through most corporate firewalls and proxies
17//! - **Simple Implementation**: Requires only basic HTTP client functionality
18//!
19//! ## URL Structure
20//!
21//! HTTP signaling info encodes the host and port information needed to
22//! construct signaling server URLs. The format is:
23//!
24//! - String representation: `/{host}/{port}`
25//! - Full URL: `http(s)://{host}:{port}/mina/webrtc/signal`
26//!
27//! ## Security Considerations
28//!
29//! HTTP signaling can use either HTTP or HTTPS depending on the signaling
30//! method variant. HTTPS is recommended for production environments to
31//! protect signaling data and prevent tampering during transmission.
32
33use std::{fmt, str::FromStr};
34
35use binprot_derive::{BinProtRead, BinProtWrite};
36use serde::{Deserialize, Serialize};
37
38use crate::webrtc::Host;
39
40use super::SignalingMethodParseError;
41
42/// HTTP signaling server connection information.
43///
44/// `HttpSignalingInfo` encapsulates the network location information needed
45/// to connect to an HTTP-based WebRTC signaling server. This includes the
46/// host address and port number required for establishing HTTP connections.
47///
48/// # Usage
49///
50/// This struct is used by both HTTP and HTTPS signaling methods, as well as
51/// HTTPS proxy configurations. It provides the fundamental addressing
52/// information needed to construct signaling URLs and establish connections.
53///
54/// # Fields
55///
56/// - `host`: The server hostname, IP address, or multiaddr
57/// - `port`: The TCP port number for the HTTP service
58///
59/// # Examples
60///
61/// ```
62/// use openmina::webrtc::Host;
63/// use openmina::signaling_method::HttpSignalingInfo;
64///
65/// // IPv4 signaling server
66/// let info = HttpSignalingInfo {
67///     host: Host::Ipv4("192.168.1.100".parse()?),
68///     port: 8080,
69/// };
70///
71/// // Domain-based signaling server
72/// let info = HttpSignalingInfo {
73///     host: Host::Domain("signal.example.com".into()),
74///     port: 443,
75/// };
76/// ```
77#[derive(BinProtWrite, BinProtRead, Eq, PartialEq, Ord, PartialOrd, Debug, Clone)]
78pub struct HttpSignalingInfo {
79    /// The host address for the HTTP signaling server.
80    ///
81    /// This can be a domain name, IPv4 address, IPv6 address, or multiaddr
82    /// depending on the network configuration and addressing requirements.
83    pub host: Host,
84
85    /// The TCP port number for the HTTP signaling server.
86    ///
87    /// Standard ports are 80 for HTTP and 443 for HTTPS, but custom
88    /// ports can be used depending on the server configuration.
89    pub port: u16,
90}
91
92impl fmt::Display for HttpSignalingInfo {
93    /// Formats the HTTP signaling info as a path component string.
94    ///
95    /// This creates a string representation suitable for inclusion in
96    /// signaling method URLs. The format is `/{host}/{port}` where the
97    /// host and port are formatted according to their respective types.
98    ///
99    /// # Example Output
100    ///
101    /// - IPv4: `/192.168.1.100/8080`
102    /// - Domain: `/signal.example.com/443`
103    /// - IPv6: `/[::1]/8080`
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "/{}/{}", self.host, self.port)
106    }
107}
108
109impl From<([u8; 4], u16)> for HttpSignalingInfo {
110    /// Creates HTTP signaling info from an IPv4 address and port tuple.
111    ///
112    /// This convenience constructor allows easy creation of `HttpSignalingInfo`
113    /// from raw IPv4 address bytes and a port number.
114    ///
115    /// # Parameters
116    ///
117    /// * `value` - A tuple containing (IPv4 address bytes, port number)
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// let info = HttpSignalingInfo::from(([192, 168, 1, 100], 8080));
123    /// assert_eq!(info.port, 8080);
124    /// ```
125    fn from(value: ([u8; 4], u16)) -> Self {
126        Self {
127            host: Host::Ipv4(value.0.into()),
128            port: value.1,
129        }
130    }
131}
132
133impl FromStr for HttpSignalingInfo {
134    type Err = SignalingMethodParseError;
135
136    /// Parses a string representation into HTTP signaling info.
137    ///
138    /// This method parses path-like strings that contain host and port
139    /// information separated by forward slashes. The expected format is
140    /// `{host}/{port}` or `/{host}/{port}`.
141    ///
142    /// # Format
143    ///
144    /// - Input: `{host}/{port}` (leading slash optional)
145    /// - Host: Domain name, IPv4, IPv6, or multiaddr format
146    /// - Port: 16-bit unsigned integer (0-65535)
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use openmina::signaling_method::HttpSignalingInfo;
152    ///
153    /// // Domain and port
154    /// let info: HttpSignalingInfo = "signal.example.com/443".parse()?;
155    ///
156    /// // IPv4 and port
157    /// let info: HttpSignalingInfo = "192.168.1.100/8080".parse()?;
158    ///
159    /// // With leading slash
160    /// let info: HttpSignalingInfo = "/localhost/8080".parse()?;
161    /// ```
162    ///
163    /// # Errors
164    ///
165    /// Returns [`SignalingMethodParseError`] for:
166    /// - Missing host or port components
167    /// - Invalid host format (not a valid hostname, IP, or multiaddr)
168    /// - Invalid port number (not a valid 16-bit unsigned integer)
169    fn from_str(s: &str) -> Result<Self, Self::Err> {
170        let mut iter = s.split('/').filter(|v| !v.trim().is_empty());
171        let host_str = iter
172            .next()
173            .ok_or(SignalingMethodParseError::NotEnoughArgs)?;
174        let host = Host::from_str(host_str)
175            .map_err(|err| SignalingMethodParseError::HostParseError(err.to_string()))?;
176
177        let port = iter
178            .next()
179            .ok_or(SignalingMethodParseError::NotEnoughArgs)?
180            .parse::<u16>()
181            .map_err(|err| SignalingMethodParseError::PortParseError(err.to_string()))?;
182
183        Ok(Self { host, port })
184    }
185}
186
187impl Serialize for HttpSignalingInfo {
188    /// Serializes the HTTP signaling info as a string.
189    ///
190    /// This uses the `Display` implementation to convert the signaling
191    /// info to its string representation for serialization. The output
192    /// format is `/{host}/{port}`.
193    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
194    where
195        S: serde::Serializer,
196    {
197        serializer.serialize_str(&self.to_string())
198    }
199}
200
201impl<'de> serde::Deserialize<'de> for HttpSignalingInfo {
202    /// Deserializes HTTP signaling info from a string.
203    ///
204    /// This uses the [`FromStr`] implementation to parse the string
205    /// representation back into an [`HttpSignalingInfo`] instance.
206    /// The expected format is `{host}/{port}` or `/{host}/{port}`.
207    ///
208    /// # Errors
209    ///
210    /// Returns a deserialization error if the string cannot be parsed
211    /// as valid HTTP signaling info (invalid host, port, or format).
212    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
213    where
214        D: serde::Deserializer<'de>,
215    {
216        let s: String = Deserialize::deserialize(deserializer)?;
217        s.parse().map_err(serde::de::Error::custom)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    //! Unit tests for HttpSignalingInfo parsing
224    //!
225    //! Run these tests with:
226    //! ```bash
227    //! cargo test -p p2p signaling_method::http::tests
228    //! ```
229
230    use super::*;
231    use crate::webrtc::Host;
232    use std::net::Ipv4Addr;
233
234    #[test]
235    fn test_from_str_valid_domain_and_port() {
236        let info: HttpSignalingInfo = "example.com/8080".parse().unwrap();
237        assert_eq!(info.host, Host::Domain("example.com".to_string()));
238        assert_eq!(info.port, 8080);
239    }
240
241    #[test]
242    fn test_from_str_valid_domain_and_port_with_leading_slash() {
243        let info: HttpSignalingInfo = "/example.com/8080".parse().unwrap();
244        assert_eq!(info.host, Host::Domain("example.com".to_string()));
245        assert_eq!(info.port, 8080);
246    }
247
248    #[test]
249    fn test_from_str_valid_ipv4_and_port() {
250        let info: HttpSignalingInfo = "192.168.1.1/443".parse().unwrap();
251        assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
252        assert_eq!(info.port, 443);
253    }
254
255    #[test]
256    fn test_from_str_valid_ipv6_and_port() {
257        let info: HttpSignalingInfo = "[::1]/8080".parse().unwrap();
258        assert!(matches!(info.host, Host::Ipv6(_)));
259        assert_eq!(info.port, 8080);
260    }
261
262    #[test]
263    fn test_from_str_valid_localhost_and_standard_ports() {
264        let info: HttpSignalingInfo = "localhost/80".parse().unwrap();
265        assert_eq!(info.host, Host::Domain("localhost".to_string()));
266        assert_eq!(info.port, 80);
267
268        let info: HttpSignalingInfo = "localhost/443".parse().unwrap();
269        assert_eq!(info.host, Host::Domain("localhost".to_string()));
270        assert_eq!(info.port, 443);
271    }
272
273    #[test]
274    fn test_from_str_valid_high_port_number() {
275        let info: HttpSignalingInfo = "example.com/65535".parse().unwrap();
276        assert_eq!(info.host, Host::Domain("example.com".to_string()));
277        assert_eq!(info.port, 65535);
278    }
279
280    #[test]
281    fn test_from_str_missing_host() {
282        let result: Result<HttpSignalingInfo, _> = "/8080".parse();
283        assert!(result.is_err());
284        assert!(matches!(
285            result.unwrap_err(),
286            SignalingMethodParseError::NotEnoughArgs
287        ));
288    }
289
290    #[test]
291    fn test_from_str_missing_port() {
292        let result: Result<HttpSignalingInfo, _> = "example.com".parse();
293        assert!(result.is_err());
294        assert!(matches!(
295            result.unwrap_err(),
296            SignalingMethodParseError::NotEnoughArgs
297        ));
298    }
299
300    #[test]
301    fn test_from_str_empty_string() {
302        let result: Result<HttpSignalingInfo, _> = "".parse();
303        assert!(result.is_err());
304        assert!(matches!(
305            result.unwrap_err(),
306            SignalingMethodParseError::NotEnoughArgs
307        ));
308    }
309
310    #[test]
311    fn test_from_str_only_slashes() {
312        let result: Result<HttpSignalingInfo, _> = "///".parse();
313        assert!(result.is_err());
314        assert!(matches!(
315            result.unwrap_err(),
316            SignalingMethodParseError::NotEnoughArgs
317        ));
318    }
319
320    #[test]
321    fn test_from_str_invalid_port_not_number() {
322        let result: Result<HttpSignalingInfo, _> = "example.com/abc".parse();
323        assert!(result.is_err());
324        assert!(matches!(
325            result.unwrap_err(),
326            SignalingMethodParseError::PortParseError(_)
327        ));
328    }
329
330    #[test]
331    fn test_from_str_invalid_port_too_large() {
332        let result: Result<HttpSignalingInfo, _> = "example.com/99999".parse();
333        assert!(result.is_err());
334        assert!(matches!(
335            result.unwrap_err(),
336            SignalingMethodParseError::PortParseError(_)
337        ));
338    }
339
340    #[test]
341    fn test_from_str_invalid_port_negative() {
342        let result: Result<HttpSignalingInfo, _> = "example.com/-1".parse();
343        assert!(result.is_err());
344        assert!(matches!(
345            result.unwrap_err(),
346            SignalingMethodParseError::PortParseError(_)
347        ));
348    }
349
350    #[test]
351    fn test_from_str_invalid_host_empty() {
352        let result: Result<HttpSignalingInfo, _> = "/8080".parse();
353        assert!(result.is_err());
354        assert!(matches!(
355            result.unwrap_err(),
356            SignalingMethodParseError::NotEnoughArgs
357        ));
358    }
359
360    #[test]
361    fn test_from_str_extra_components_ignored() {
362        // Should only use first two non-empty components
363        let info: HttpSignalingInfo = "example.com/8080/extra/stuff".parse().unwrap();
364        assert_eq!(info.host, Host::Domain("example.com".to_string()));
365        assert_eq!(info.port, 8080);
366    }
367
368    #[test]
369    fn test_from_str_whitespace_in_components() {
370        // Components with whitespace should be trimmed by the split filter
371        let result: Result<HttpSignalingInfo, _> = "   /  /8080".parse();
372        assert!(result.is_err());
373        assert!(matches!(
374            result.unwrap_err(),
375            SignalingMethodParseError::NotEnoughArgs
376        ));
377    }
378
379    #[test]
380    fn test_roundtrip_display_and_from_str() {
381        let original = HttpSignalingInfo {
382            host: Host::Domain("signal.example.com".to_string()),
383            port: 443,
384        };
385
386        let serialized = original.to_string();
387        let deserialized: HttpSignalingInfo = serialized.parse().unwrap();
388
389        assert_eq!(original, deserialized);
390    }
391
392    #[test]
393    fn test_roundtrip_ipv4() {
394        let original = HttpSignalingInfo {
395            host: Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)),
396            port: 8080,
397        };
398
399        let serialized = original.to_string();
400        let deserialized: HttpSignalingInfo = serialized.parse().unwrap();
401
402        assert_eq!(original, deserialized);
403    }
404}