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}