Skip to main content

mina_p2p/webrtc/signaling_method/
mod.rs

1//! WebRTC Signaling Transport Methods
2//!
3//! This module defines the different transport methods available for WebRTC signaling
4//! in Mina Rust's peer-to-peer network. WebRTC requires an external signaling mechanism
5//! to exchange connection metadata before establishing direct peer-to-peer connections.
6//!
7//! ## Signaling Transport Methods
8//!
9//! The Mina Rust node supports multiple signaling transport methods to accommodate different
10//! network environments and security requirements:
11//!
12//! ### HTTP/HTTPS Direct Connections
13//!
14//! - **HTTP**: Direct HTTP connections to signaling servers (typically for local/testing)
15//! - **HTTPS**: Secure HTTPS connections to signaling servers (recommended for production)
16//!
17//! These methods allow peers to directly contact signaling servers to exchange offers
18//! and answers for WebRTC connection establishment.
19//!
20//! ### HTTPS Proxy
21//!
22//! - **HTTPS Proxy**: Uses an SSL gateway/proxy server to reach the actual signaling server
23//!
24//! ### P2P Relay Signaling
25//!
26//! - **P2P Relay**: Uses existing peer connections to relay signaling messages
27//! - Enables signaling through already-established peer connections
28//! - Provides redundancy when direct signaling server access is unavailable
29//! - Supports bootstrapping new connections through existing network peers
30//!
31//! ## URL Format
32//!
33//! Signaling methods use a structured URL format:
34//!
35//! - HTTP: `/http/{host}/{port}`
36//! - HTTPS: `/https/{host}/{port}`
37//! - HTTPS Proxy (legacy): `/https_proxy/{cluster_id}/{host}/{port}`
38//! - Proxied: `/proxied/{http|https}/{encoded_prefix}/{host}/{port}`
39//! - P2P Relay: `/p2p/{peer_id}`
40//!
41//! ## Connection Strategy
42//!
43//! The signaling method determines how peers discover and connect to each other:
44//!
45//! 1. **Direct Methods** (HTTP/HTTPS) - Can connect immediately to signaling servers
46//! 2. **Proxy Methods** - Route through intermediate proxy infrastructure
47//! 3. **Relay Methods** - Require existing peer connections for message routing
48
49mod http;
50pub use http::HttpSignalingInfo;
51
52use std::{borrow::Cow, fmt, str::FromStr};
53
54use binprot::{BinProtRead, BinProtWrite};
55use binprot_derive::{BinProtRead, BinProtWrite};
56use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
57use serde::{Deserialize, Serialize};
58use thiserror::Error;
59
60use crate::PeerId;
61
62/// URL path prefix for proxy signaling.
63///
64/// This newtype wraps a String path prefix (e.g., "/clusters/123") and provides
65/// BinProt serialization by encoding as a length-prefixed byte array.
66///
67/// Used by `Proxied` variant for flexible path-based proxy configurations.
68/// The legacy `HttpsProxy(u16, HttpSignalingInfo)` is preserved for BinProt
69/// backward compatibility.
70#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, derive_more::Display)]
71pub struct PathPrefix(String);
72
73impl PathPrefix {
74    /// Consumes the PathPrefix and returns the inner String.
75    pub fn into_inner(self) -> String {
76        self.0
77    }
78}
79
80impl From<String> for PathPrefix {
81    fn from(s: String) -> Self {
82        Self(s)
83    }
84}
85
86impl From<&str> for PathPrefix {
87    fn from(s: &str) -> Self {
88        Self(s.to_string())
89    }
90}
91
92impl From<Cow<'_, str>> for PathPrefix {
93    fn from(s: Cow<'_, str>) -> Self {
94        Self(s.into_owned())
95    }
96}
97
98impl<'a> From<&'a PathPrefix> for Cow<'a, str> {
99    fn from(p: &'a PathPrefix) -> Self {
100        Cow::Borrowed(p.as_ref())
101    }
102}
103
104impl AsRef<str> for PathPrefix {
105    fn as_ref(&self) -> &str {
106        &self.0
107    }
108}
109
110/// Proxy connection scheme (HTTP or HTTPS).
111///
112/// Determines whether the proxy connection uses plain HTTP or secure HTTPS.
113/// HTTPS is recommended for production environments.
114#[derive(
115    BinProtWrite,
116    BinProtRead,
117    Eq,
118    PartialEq,
119    Ord,
120    PartialOrd,
121    Debug,
122    Clone,
123    Copy,
124    derive_more::Display,
125)]
126pub enum ProxyScheme {
127    /// Plain HTTP proxy connection.
128    #[display(fmt = "http")]
129    Http,
130    /// Secure HTTPS proxy connection.
131    #[display(fmt = "https")]
132    Https,
133}
134
135impl BinProtRead for PathPrefix {
136    fn binprot_read<R: std::io::Read + ?Sized>(r: &mut R) -> Result<Self, binprot::Error>
137    where
138        Self: Sized,
139    {
140        let bytes: Vec<u8> = BinProtRead::binprot_read(r)?;
141        let s = String::from_utf8(bytes).map_err(|e| binprot::Error::from(e.utf8_error()))?;
142        Ok(s.into())
143    }
144}
145
146impl BinProtWrite for PathPrefix {
147    fn binprot_write<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
148        self.as_ref().as_bytes().to_vec().binprot_write(w)
149    }
150}
151
152/// WebRTC signaling transport method configuration.
153///
154/// `SignalingMethod` defines how WebRTC signaling messages (offers and answers)
155/// are transported between peers. Different methods provide flexibility for
156/// various network environments and infrastructure requirements.
157///
158/// # Method Types
159///
160/// - **HTTP/HTTPS**: Direct connections to signaling servers
161/// - **HTTPS Proxy**: Connections through SSL gateway/proxy servers
162/// - **P2P Relay**: Signaling through existing peer connections
163///
164/// Each method encapsulates the necessary connection information to establish
165/// the signaling channel, which is used before the actual WebRTC peer-to-peer
166/// connection is established.
167///
168/// # Usage
169///
170/// Signaling methods can be parsed from string representations or constructed
171/// programmatically. They support serialization for storage and network transmission.
172///
173/// # Example
174///
175/// ```
176/// // Direct HTTPS signaling
177/// let method = "/https/signal.example.com/443".parse::<SignalingMethod>()?;
178///
179/// // P2P relay through an existing peer
180/// let method = SignalingMethod::P2p { relay_peer_id: peer_id };
181/// ```
182#[derive(BinProtWrite, BinProtRead, Eq, PartialEq, Ord, PartialOrd, Debug, Clone)]
183pub enum SignalingMethod {
184    /// HTTP signaling server connection.
185    ///
186    /// Uses plain HTTP for signaling message exchange. Typically used for
187    /// local development or testing environments where encryption is not required.
188    Http(HttpSignalingInfo),
189
190    /// HTTPS signaling server connection.
191    ///
192    /// Uses secure HTTPS for signaling message exchange. Recommended for
193    /// production environments to protect signaling data in transit.
194    Https(HttpSignalingInfo),
195
196    /// HTTPS proxy signaling connection (legacy format).
197    ///
198    /// Uses an SSL gateway/proxy server to reach the actual signaling server.
199    /// The first parameter is the cluster ID for routing, and the second
200    /// parameter contains the proxy server connection information.
201    ///
202    /// Kept for BinProt backward compatibility. Prefer `Proxied` for new code.
203    HttpsProxy(u16, HttpSignalingInfo),
204
205    /// P2P relay signaling through an existing peer connection.
206    ///
207    /// Uses an already-established peer connection to relay signaling messages
208    /// to other peers. This enables signaling when direct access to signaling
209    /// servers is unavailable and provides redundancy in the signaling process.
210    P2p {
211        /// The peer ID of the relay peer that will forward signaling messages.
212        relay_peer_id: PeerId,
213    },
214
215    /// Proxy signaling connection (extended format).
216    ///
217    /// Uses a gateway/proxy server to reach the actual signaling server.
218    /// Supports both HTTP and HTTPS proxy connections via the `ProxyScheme` field.
219    ///
220    /// Fields:
221    /// - `ProxyScheme`: Whether to use HTTP or HTTPS for the proxy connection
222    /// - `PathPrefix`: The URL path prefix (e.g., "/clusters/123")
223    /// - `HttpSignalingInfo`: The proxy server connection information
224    Proxied(ProxyScheme, PathPrefix, HttpSignalingInfo),
225}
226
227impl SignalingMethod {
228    /// Determines if this signaling method supports direct connections.
229    ///
230    /// Direct connection methods (HTTP, HTTPS, HTTPS Proxy, Proxied) can establish
231    /// signaling channels immediately without requiring existing peer connections.
232    /// P2P relay methods require an already-established peer connection to function.
233    ///
234    /// # Returns
235    ///
236    /// * `true` for HTTP, HTTPS, HTTPS Proxy, and Proxied methods
237    /// * `false` for P2P relay methods
238    ///
239    /// This is useful for connection strategy decisions and determining whether
240    /// bootstrap connections are needed before signaling can occur.
241    pub fn can_connect_directly(&self) -> bool {
242        !matches!(self, Self::P2p { .. })
243    }
244
245    /// Constructs the HTTP(S) URL for sending WebRTC offers.
246    ///
247    /// This method generates the appropriate URL endpoint for sending WebRTC
248    /// signaling messages based on the signaling method configuration.
249    ///
250    /// # URL Formats
251    ///
252    /// - **HTTP**: `http://{host}:{port}/mina/webrtc/signal`
253    /// - **HTTPS**: `https://{host}:{port}/mina/webrtc/signal`
254    /// - **HTTPS Proxy**: `https://{host}:{port}/clusters/{cluster_id}/mina/webrtc/signal`
255    /// - **Proxied**: `{http|https}://{host}:{port}{prefix}/mina/webrtc/signal`
256    ///
257    /// # Returns
258    ///
259    /// * `Some(String)` containing the signaling URL for HTTP-based methods
260    /// * `None` for P2P relay methods that don't use HTTP endpoints
261    ///
262    /// # Example
263    ///
264    /// ```
265    /// let method = SignalingMethod::Https(info);
266    /// let url = method.http_url(); // Some("https://signal.example.com:443/mina/webrtc/signal")
267    /// ```
268    pub fn http_url(&self) -> Option<String> {
269        let slash = Cow::Borrowed("/");
270        let (http, prefix, HttpSignalingInfo { host, port }) = match self {
271            Self::Http(info) => ("http", slash, info),
272            Self::Https(info) => ("https", slash, info),
273            Self::HttpsProxy(cluster_id, info) => (
274                "https",
275                Cow::Owned(format!("/clusters/{cluster_id}/")),
276                info,
277            ),
278            Self::Proxied(scheme, prefix, info) => {
279                // Handle empty prefix or just "/" as equivalent to no prefix
280                let prefix_str = prefix.as_ref();
281                let prefix_cow = if prefix_str.is_empty() || prefix_str == "/" {
282                    slash
283                } else {
284                    let needs_start_slash = !prefix_str.starts_with('/');
285                    let needs_end_slash = !prefix_str.ends_with('/');
286                    Cow::Owned(format!(
287                        "{}{}{}",
288                        if needs_start_slash { "/" } else { "" },
289                        prefix_str,
290                        if needs_end_slash { "/" } else { "" }
291                    ))
292                };
293                return Some(format!(
294                    "{scheme}://{host}:{port}{prefix_cow}mina/webrtc/signal",
295                    host = info.host,
296                    port = info.port
297                ));
298            }
299            _ => return None,
300        };
301        Some(format!("{http}://{host}:{port}{prefix}mina/webrtc/signal",))
302    }
303
304    /// Extracts the relay peer ID for P2P signaling methods.
305    ///
306    /// For P2P relay signaling methods, this returns the peer ID of the
307    /// intermediate peer that will forward signaling messages. This is used
308    /// to identify which existing peer connection should be used for relaying.
309    ///
310    /// # Returns
311    ///
312    /// * `Some(PeerId)` for P2P relay methods
313    /// * `None` for direct connection methods (HTTP/HTTPS)
314    ///
315    /// # Usage
316    ///
317    /// This method is typically used when setting up message routing for
318    /// P2P relay signaling to determine which peer connection should handle
319    /// the signaling traffic.
320    pub fn p2p_relay_peer_id(&self) -> Option<PeerId> {
321        match self {
322            Self::P2p { relay_peer_id } => Some(*relay_peer_id),
323            _ => None,
324        }
325    }
326}
327
328impl fmt::Display for SignalingMethod {
329    /// Formats the signaling method as a URL path string.
330    ///
331    /// This implementation converts the signaling method into its string
332    /// representation following the URL format patterns. The formatted
333    /// string can be parsed back using [`FromStr`].
334    ///
335    /// # Format Patterns
336    ///
337    /// - HTTP: `/http/{host}/{port}`
338    /// - HTTPS: `/https/{host}/{port}`
339    /// - HTTPS Proxy: `/https_proxy/{cluster_id}/{host}/{port}`
340    /// - P2P Relay: `/p2p/{peer_id}`
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        match self {
343            Self::Http(signaling) => {
344                write!(f, "/http")?;
345                signaling.fmt(f)
346            }
347            Self::Https(signaling) => {
348                write!(f, "/https")?;
349                signaling.fmt(f)
350            }
351            Self::HttpsProxy(cluster_id, signaling) => {
352                write!(f, "/https_proxy/{cluster_id}")?;
353                signaling.fmt(f)
354            }
355            Self::Proxied(scheme, path_prefix, signaling) => {
356                let encoded = utf8_percent_encode(path_prefix.as_ref(), NON_ALPHANUMERIC);
357                write!(f, "/proxied/{scheme}/{encoded}")?;
358                signaling.fmt(f)
359            }
360            Self::P2p { relay_peer_id } => {
361                write!(f, "/p2p/{relay_peer_id}")
362            }
363        }
364    }
365}
366
367/// Errors that can occur when parsing signaling method strings.
368///
369/// `SignalingMethodParseError` provides detailed error information for
370/// parsing failures when converting string representations to [`SignalingMethod`]
371/// instances. This helps with debugging configuration and user input validation.
372///
373/// # Error Types
374///
375/// The parser can fail for various reasons including missing components,
376/// invalid formats, or unsupported method types. Each error variant provides
377/// specific context about what went wrong during parsing.
378#[derive(Error, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
379pub enum SignalingMethodParseError {
380    /// Insufficient arguments provided for the signaling method.
381    ///
382    /// This occurs when the input string doesn't contain enough components
383    /// to construct a valid signaling method. For example, missing host
384    /// or port information for HTTP methods.
385    #[error("not enough args for the signaling method")]
386    NotEnoughArgs,
387
388    /// Unknown or unsupported signaling method type.
389    ///
390    /// This occurs when the method type (first component) is not recognized.
391    /// Supported methods are: `http`, `https`, `https_proxy`, `p2p`.
392    #[error("unknown signaling method: `{0}`")]
393    UnknownSignalingMethod(String),
394
395    /// Invalid cluster ID for HTTPS proxy methods.
396    ///
397    /// This occurs when the cluster ID component cannot be parsed as a
398    /// valid 16-bit unsigned integer for HTTPS proxy configurations.
399    #[error("invalid cluster id")]
400    InvalidClusterId,
401
402    /// Failed to parse the host component.
403    ///
404    /// This occurs when the host string cannot be parsed as a valid
405    /// hostname, IP address, or multiaddr format by the Host parser.
406    #[error("host parse error: {0}")]
407    HostParseError(String),
408
409    /// Failed to parse the port component.
410    ///
411    /// This occurs when the port string cannot be parsed as a valid
412    /// 16-bit unsigned integer port number.
413    #[error("port parse error: {0}")]
414    PortParseError(String),
415}
416
417impl FromStr for SignalingMethod {
418    type Err = SignalingMethodParseError;
419
420    /// Parses a string representation into a [`SignalingMethod`].
421    ///
422    /// This method parses URL-like strings that represent different signaling
423    /// transport methods. The parser supports the following formats:
424    ///
425    /// # Supported Formats
426    ///
427    /// - **HTTP**: `/http/{host}/{port}`
428    /// - **HTTPS**: `/https/{host}/{port}`
429    /// - **HTTPS Proxy**: `/https_proxy/{cluster_id}/{host}/{port}`
430    /// - **P2P Relay**: `/p2p/{peer_id}`
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use mina::signaling_method::SignalingMethod;
436    ///
437    /// // HTTP signaling
438    /// let method: SignalingMethod = "/http/localhost/8080".parse()?;
439    ///
440    /// // HTTPS signaling
441    /// let method: SignalingMethod = "/https/signal.example.com/443".parse()?;
442    ///
443    /// // HTTPS proxy with cluster ID
444    /// let method: SignalingMethod = "/https_proxy/123/proxy.example.com/443".parse()?;
445    /// ```
446    ///
447    /// # Errors
448    ///
449    /// Returns [`SignalingMethodParseError`] for various parsing failures:
450    /// - Missing components (host, port, etc.)
451    /// - Unknown method types
452    /// - Invalid numeric values (ports, cluster IDs)
453    /// - Invalid host formats
454    fn from_str(s: &str) -> Result<Self, Self::Err> {
455        if s.is_empty() {
456            return Err(SignalingMethodParseError::NotEnoughArgs);
457        }
458
459        let method_end_index = s[1..]
460            .find('/')
461            .map(|i| i + 1)
462            .filter(|i| s.len() > *i)
463            .ok_or(SignalingMethodParseError::NotEnoughArgs)?;
464
465        let rest = &s[method_end_index..];
466        match &s[1..method_end_index] {
467            "http" => Ok(Self::Http(rest.parse()?)),
468            "https" => Ok(Self::Https(rest.parse()?)),
469            "https_proxy" => {
470                let mut iter = rest.splitn(3, '/').filter(|v| !v.trim().is_empty());
471                let (cluster_id, rest) = (
472                    iter.next()
473                        .ok_or(SignalingMethodParseError::NotEnoughArgs)?,
474                    iter.next()
475                        .ok_or(SignalingMethodParseError::NotEnoughArgs)?,
476                );
477                let cluster_id: u16 = cluster_id
478                    .parse()
479                    .or(Err(SignalingMethodParseError::InvalidClusterId))?;
480                Ok(Self::HttpsProxy(cluster_id, rest.parse()?))
481            }
482            "proxied" => {
483                // Format: /proxied/{scheme}/{encoded_prefix}/{host}/{port}
484                let mut iter = rest.splitn(4, '/').filter(|v| !v.trim().is_empty());
485                let scheme_str = iter
486                    .next()
487                    .ok_or(SignalingMethodParseError::NotEnoughArgs)?;
488                let scheme = match scheme_str {
489                    "http" => ProxyScheme::Http,
490                    "https" => ProxyScheme::Https,
491                    _ => {
492                        return Err(SignalingMethodParseError::UnknownSignalingMethod(format!(
493                            "proxied/{}",
494                            scheme_str
495                        )))
496                    }
497                };
498                let encoded_prefix = iter
499                    .next()
500                    .ok_or(SignalingMethodParseError::NotEnoughArgs)?;
501                let rest = iter
502                    .next()
503                    .ok_or(SignalingMethodParseError::NotEnoughArgs)?;
504                let path_prefix = percent_decode_str(encoded_prefix)
505                    .decode_utf8()
506                    .map_err(|e| SignalingMethodParseError::HostParseError(e.to_string()))?
507                    .into_owned();
508                Ok(Self::Proxied(scheme, path_prefix.into(), rest.parse()?))
509            }
510            method => Err(SignalingMethodParseError::UnknownSignalingMethod(
511                method.to_owned(),
512            )),
513        }
514    }
515}
516
517impl Serialize for SignalingMethod {
518    /// Serializes the signaling method as a string.
519    ///
520    /// This uses the `Display` implementation to convert the signaling
521    /// method to its string representation for serialization.
522    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
523    where
524        S: serde::Serializer,
525    {
526        serializer.serialize_str(&self.to_string())
527    }
528}
529
530impl<'de> serde::Deserialize<'de> for SignalingMethod {
531    /// Deserializes a signaling method from a string.
532    ///
533    /// This uses the [`FromStr`] implementation to parse the string
534    /// representation back into a [`SignalingMethod`] instance.
535    ///
536    /// # Errors
537    ///
538    /// Returns a deserialization error if the string cannot be parsed
539    /// as a valid signaling method.
540    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
541    where
542        D: serde::Deserializer<'de>,
543    {
544        let s: String = Deserialize::deserialize(deserializer)?;
545        s.parse().map_err(serde::de::Error::custom)
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    //! Unit tests for SignalingMethod parsing
552    //!
553    //! Run these tests with:
554    //! ```bash
555    //! cargo test -p p2p signaling_method::tests
556    //! ```
557
558    use super::*;
559    use crate::webrtc::Host;
560    use std::net::Ipv4Addr;
561
562    #[test]
563    fn test_from_str_valid_http() {
564        let method: SignalingMethod = "/http/example.com/8080".parse().unwrap();
565        match method {
566            SignalingMethod::Http(info) => {
567                assert_eq!(info.host, Host::Domain("example.com".to_string()));
568                assert_eq!(info.port, 8080);
569            }
570            x => panic!("Expected Http variant, got {x:?}"),
571        }
572    }
573
574    #[test]
575    fn test_from_str_valid_https() {
576        let method: SignalingMethod = "/https/signal.example.com/443".parse().unwrap();
577        match method {
578            SignalingMethod::Https(info) => {
579                assert_eq!(info.host, Host::Domain("signal.example.com".to_string()));
580                assert_eq!(info.port, 443);
581            }
582            x => panic!("Expected Https variant, got {x:?}"),
583        }
584    }
585
586    #[test]
587    fn test_from_str_valid_https_proxy() {
588        let method: SignalingMethod = "/https_proxy/123/proxy.example.com/443".parse().unwrap();
589        match method {
590            SignalingMethod::HttpsProxy(cluster_id, info) => {
591                assert_eq!(cluster_id, 123);
592                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
593                assert_eq!(info.port, 443);
594            }
595            x => panic!("Expected HttpsProxy variant, got {x:?}"),
596        }
597    }
598
599    #[test]
600    fn test_from_str_valid_https_proxy_max_cluster_id() {
601        let method: SignalingMethod = "/https_proxy/65535/proxy.example.com/443".parse().unwrap();
602        match method {
603            SignalingMethod::HttpsProxy(cluster_id, info) => {
604                assert_eq!(cluster_id, 65535);
605                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
606                assert_eq!(info.port, 443);
607            }
608            x => panic!("Expected HttpsProxy variant, got {x:?}"),
609        }
610    }
611
612    #[test]
613    fn test_from_str_valid_http_ipv4() {
614        let method: SignalingMethod = "/http/192.168.1.1/8080".parse().unwrap();
615        match method {
616            SignalingMethod::Http(info) => {
617                assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
618                assert_eq!(info.port, 8080);
619            }
620            x => panic!("Expected Http variant, got {x:?}"),
621        }
622    }
623
624    #[test]
625    fn test_from_str_valid_https_ipv6() {
626        let method: SignalingMethod = "/https/[::1]/443".parse().unwrap();
627        match method {
628            SignalingMethod::Https(info) => {
629                assert!(matches!(info.host, Host::Ipv6(_)));
630                assert_eq!(info.port, 443);
631            }
632            x => panic!("Expected Https variant, got {x:?}"),
633        }
634    }
635
636    #[test]
637    fn test_from_str_empty_string() {
638        let result: Result<SignalingMethod, _> = "".parse();
639        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
640    }
641
642    #[test]
643    fn test_from_str_no_leading_slash() {
644        let result: Result<SignalingMethod, _> = "http/example.com/8080".parse();
645        // Without leading slash, it parses "ttp" as the method (s[1..] gives
646        // "ttp/example.com/8080")
647        assert_eq!(
648            result,
649            Err(SignalingMethodParseError::UnknownSignalingMethod(
650                "ttp".to_string()
651            ))
652        );
653    }
654
655    #[test]
656    fn test_from_str_only_slash() {
657        let result: Result<SignalingMethod, _> = "/".parse();
658        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
659    }
660
661    #[test]
662    fn test_from_str_unknown_method() {
663        let result: Result<SignalingMethod, _> = "/websocket/example.com/8080".parse();
664        assert_eq!(
665            result,
666            Err(SignalingMethodParseError::UnknownSignalingMethod(
667                "websocket".to_string()
668            ))
669        );
670    }
671
672    #[test]
673    fn test_from_str_unknown_method_with_valid_format() {
674        let result: Result<SignalingMethod, _> = "/ftp/example.com/21".parse();
675        assert_eq!(
676            result,
677            Err(SignalingMethodParseError::UnknownSignalingMethod(
678                "ftp".to_string()
679            ))
680        );
681    }
682
683    #[test]
684    fn test_from_str_http_missing_host() {
685        let result: Result<SignalingMethod, _> = "/http".parse();
686        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
687    }
688
689    #[test]
690    fn test_from_str_http_missing_port() {
691        let result: Result<SignalingMethod, _> = "/http/example.com".parse();
692        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
693    }
694
695    #[test]
696    fn test_from_str_http_invalid_port() {
697        let result: Result<SignalingMethod, _> = "/http/example.com/abc".parse();
698        assert!(
699            matches!(result, Err(SignalingMethodParseError::PortParseError(_))),
700            "expected PortParseError, got {result:?}"
701        );
702    }
703
704    #[test]
705    fn test_from_str_http_port_too_large() {
706        let result: Result<SignalingMethod, _> = "/http/example.com/99999".parse();
707        assert!(
708            matches!(result, Err(SignalingMethodParseError::PortParseError(_))),
709            "expected PortParseError, got {result:?}"
710        );
711    }
712
713    #[test]
714    fn test_from_str_https_proxy_missing_cluster_id() {
715        let result: Result<SignalingMethod, _> = "/https_proxy".parse();
716        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
717    }
718
719    #[test]
720    fn test_from_str_https_proxy_missing_host() {
721        let result: Result<SignalingMethod, _> = "/https_proxy/123".parse();
722        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
723    }
724
725    #[test]
726    fn test_from_str_https_proxy_invalid_cluster_id() {
727        let result: Result<SignalingMethod, _> = "/https_proxy/abc/proxy.example.com/443".parse();
728        assert_eq!(result, Err(SignalingMethodParseError::InvalidClusterId));
729    }
730
731    #[test]
732    fn test_from_str_https_proxy_cluster_id_too_large() {
733        let result: Result<SignalingMethod, _> = "/https_proxy/99999/proxy.example.com/443".parse();
734        assert_eq!(result, Err(SignalingMethodParseError::InvalidClusterId));
735    }
736
737    #[test]
738    fn test_from_str_https_proxy_negative_cluster_id() {
739        let result: Result<SignalingMethod, _> = "/https_proxy/-1/proxy.example.com/443".parse();
740        assert_eq!(result, Err(SignalingMethodParseError::InvalidClusterId));
741    }
742
743    #[test]
744    fn test_from_str_invalid_host() {
745        // This will depend on Host's parsing behavior - assuming it rejects
746        // certain formats
747        let result: Result<SignalingMethod, _> = "/http//8080".parse();
748        // Should be either NotEnoughArgs or HostParseError depending on
749        // implementation
750        assert!(
751            matches!(
752                result,
753                Err(SignalingMethodParseError::NotEnoughArgs)
754                    | Err(SignalingMethodParseError::HostParseError(_))
755            ),
756            "expected NotEnoughArgs or HostParseError, got {result:?}"
757        );
758    }
759
760    #[test]
761    fn test_from_str_extra_slashes() {
762        let result: Result<SignalingMethod, _> = "//http//example.com//8080//".parse();
763        // The double leading slashes mean s[1..] gives "/http//...", split
764        // produces empty first component
765        assert_eq!(
766            result,
767            Err(SignalingMethodParseError::UnknownSignalingMethod(
768                "".to_string()
769            ))
770        );
771    }
772
773    #[test]
774    fn test_roundtrip_http() {
775        let original = SignalingMethod::Http(HttpSignalingInfo {
776            host: Host::Domain("example.com".to_string()),
777            port: 8080,
778        });
779
780        let serialized = original.to_string();
781        let deserialized: SignalingMethod = serialized.parse().unwrap();
782
783        assert_eq!(original, deserialized);
784    }
785
786    #[test]
787    fn test_roundtrip_https() {
788        let original = SignalingMethod::Https(HttpSignalingInfo {
789            host: Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)),
790            port: 443,
791        });
792
793        let serialized = original.to_string();
794        let deserialized: SignalingMethod = serialized.parse().unwrap();
795
796        assert_eq!(original, deserialized);
797    }
798
799    #[test]
800    fn test_roundtrip_https_proxy() {
801        let original = SignalingMethod::HttpsProxy(
802            123,
803            HttpSignalingInfo {
804                host: Host::Domain("proxy.example.com".to_string()),
805                port: 443,
806            },
807        );
808
809        let serialized = original.to_string();
810        let deserialized: SignalingMethod = serialized.parse().unwrap();
811
812        assert_eq!(original, deserialized);
813    }
814
815    #[test]
816    fn test_case_sensitivity() {
817        let result: Result<SignalingMethod, _> = "/HTTP/example.com/8080".parse();
818        assert_eq!(
819            result,
820            Err(SignalingMethodParseError::UnknownSignalingMethod(
821                "HTTP".to_string()
822            ))
823        );
824
825        let result: Result<SignalingMethod, _> = "/Http/example.com/8080".parse();
826        assert_eq!(
827            result,
828            Err(SignalingMethodParseError::UnknownSignalingMethod(
829                "Http".to_string()
830            ))
831        );
832    }
833
834    #[test]
835    fn test_whitespace_handling() {
836        // The parser should filter empty components from split
837        let result: Result<SignalingMethod, _> = "/http/ /8080".parse();
838        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
839    }
840
841    #[test]
842    fn test_https_proxy_zero_cluster_id() {
843        let method: SignalingMethod = "/https_proxy/0/proxy.example.com/443".parse().unwrap();
844        match method {
845            SignalingMethod::HttpsProxy(cluster_id, info) => {
846                assert_eq!(cluster_id, 0);
847                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
848                assert_eq!(info.port, 443);
849            }
850            x => panic!("Expected HttpsProxy variant, got {x:?}"),
851        }
852    }
853
854    #[test]
855    fn test_standard_ports() {
856        let method: SignalingMethod = "/http/localhost/80".parse().unwrap();
857        match method {
858            SignalingMethod::Http(info) => {
859                assert_eq!(info.port, 80);
860            }
861            x => panic!("Expected Http variant, got {x:?}"),
862        }
863
864        let method: SignalingMethod = "/https/localhost/443".parse().unwrap();
865        match method {
866            SignalingMethod::Https(info) => {
867                assert_eq!(info.port, 443);
868            }
869            x => panic!("Expected Https variant, got {x:?}"),
870        }
871    }
872
873    #[test]
874    fn test_https_proxy_with_ipv4() {
875        let method: SignalingMethod = "/https_proxy/456/192.168.1.1/8443".parse().unwrap();
876        match method {
877            SignalingMethod::HttpsProxy(cluster_id, info) => {
878                assert_eq!(cluster_id, 456);
879                assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
880                assert_eq!(info.port, 8443);
881            }
882            x => panic!("Expected HttpsProxy variant, got {x:?}"),
883        }
884    }
885
886    // Proxied tests
887
888    #[test]
889    fn test_from_str_valid_proxied() {
890        // URL-encoded path prefix: /clusters/123 -> %2Fclusters%2F123
891        let method: SignalingMethod = "/proxied/https/%2Fclusters%2F123/proxy.example.com/443"
892            .parse()
893            .unwrap();
894        match method {
895            SignalingMethod::Proxied(ProxyScheme::Https, prefix, info) => {
896                assert_eq!(prefix.as_ref(), "/clusters/123");
897                assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
898                assert_eq!(info.port, 443);
899            }
900            x => panic!("Expected Proxied variant, got {x:?}"),
901        }
902    }
903
904    #[test]
905    fn test_from_str_proxied_complex_path() {
906        // URL-encoded path: /api/v2/webrtc -> %2Fapi%2Fv2%2Fwebrtc
907        let method: SignalingMethod =
908            "/proxied/https/%2Fapi%2Fv2%2Fwebrtc/gateway.example.com/8443"
909                .parse()
910                .unwrap();
911        match method {
912            SignalingMethod::Proxied(ProxyScheme::Https, prefix, info) => {
913                assert_eq!(prefix.as_ref(), "/api/v2/webrtc");
914                assert_eq!(info.host, Host::Domain("gateway.example.com".to_string()));
915                assert_eq!(info.port, 8443);
916            }
917            x => panic!("Expected Proxied variant, got {x:?}"),
918        }
919    }
920
921    #[test]
922    fn test_roundtrip_proxied() {
923        let original = SignalingMethod::Proxied(
924            ProxyScheme::Https,
925            "/clusters/789".into(),
926            HttpSignalingInfo {
927                host: Host::Domain("proxy.example.com".to_string()),
928                port: 443,
929            },
930        );
931
932        let serialized = original.to_string();
933        assert_eq!(
934            serialized,
935            "/proxied/https/%2Fclusters%2F789/proxy.example.com/443"
936        );
937
938        let deserialized: SignalingMethod = serialized.parse().unwrap();
939        assert_eq!(original, deserialized);
940    }
941
942    #[test]
943    fn test_proxied_https_url() {
944        let method = SignalingMethod::Proxied(
945            ProxyScheme::Https,
946            "/custom/path".into(),
947            HttpSignalingInfo {
948                host: Host::Domain("gateway.example.com".to_string()),
949                port: 443,
950            },
951        );
952
953        let url = method.http_url().unwrap();
954        assert_eq!(
955            url,
956            "https://gateway.example.com:443/custom/path/mina/webrtc/signal"
957        );
958    }
959
960    #[test]
961    fn test_proxied_https_url_no_leading_slash() {
962        let method = SignalingMethod::Proxied(
963            ProxyScheme::Https,
964            "custom/path".into(),
965            HttpSignalingInfo {
966                host: Host::Domain("gateway.example.com".to_string()),
967                port: 443,
968            },
969        );
970
971        let url = method.http_url().unwrap();
972        // Should add leading slash
973        assert_eq!(
974            url,
975            "https://gateway.example.com:443/custom/path/mina/webrtc/signal"
976        );
977    }
978
979    #[test]
980    fn test_proxied_https_url_trailing_slash() {
981        let method = SignalingMethod::Proxied(
982            ProxyScheme::Https,
983            "/custom/path/".into(),
984            HttpSignalingInfo {
985                host: Host::Domain("gateway.example.com".to_string()),
986                port: 443,
987            },
988        );
989
990        let url = method.http_url().unwrap();
991        // Should not double the trailing slash
992        assert_eq!(
993            url,
994            "https://gateway.example.com:443/custom/path/mina/webrtc/signal"
995        );
996    }
997
998    #[test]
999    fn test_proxied_with_ipv4() {
1000        let method: SignalingMethod = "/proxied/https/%2Ftest/192.168.1.1/8443".parse().unwrap();
1001        match method {
1002            SignalingMethod::Proxied(ProxyScheme::Https, prefix, info) => {
1003                assert_eq!(prefix.as_ref(), "/test");
1004                assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1)));
1005                assert_eq!(info.port, 8443);
1006            }
1007            _ => panic!("Expected Proxied variant"),
1008        }
1009    }
1010
1011    #[test]
1012    fn test_proxied_missing_prefix() {
1013        let result: Result<SignalingMethod, _> = "/proxied".parse();
1014        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
1015    }
1016
1017    #[test]
1018    fn test_proxied_missing_host() {
1019        let result: Result<SignalingMethod, _> = "/proxied/https/%2Fprefix".parse();
1020        assert_eq!(result, Err(SignalingMethodParseError::NotEnoughArgs));
1021    }
1022
1023    // HttpsProxy vs Proxied equivalency tests
1024
1025    #[test]
1026    fn test_https_proxy_and_proxied_equivalent_url() {
1027        let info = HttpSignalingInfo {
1028            host: Host::Domain("gateway.example.com".to_string()),
1029            port: 443,
1030        };
1031
1032        let legacy = SignalingMethod::HttpsProxy(123, info.clone());
1033        let proxied = SignalingMethod::Proxied(ProxyScheme::Https, "/clusters/123".into(), info);
1034
1035        assert_eq!(legacy.http_url(), proxied.http_url());
1036        assert_eq!(
1037            legacy.http_url().unwrap(),
1038            "https://gateway.example.com:443/clusters/123/mina/webrtc/signal"
1039        );
1040    }
1041
1042    #[test]
1043    fn test_proxied_empty_prefix() {
1044        let method = SignalingMethod::Proxied(
1045            ProxyScheme::Https,
1046            "".into(),
1047            HttpSignalingInfo {
1048                host: Host::Domain("gateway.example.com".to_string()),
1049                port: 443,
1050            },
1051        );
1052
1053        let url = method.http_url().unwrap();
1054        // Empty prefix should still result in valid URL with single slash
1055        assert_eq!(url, "https://gateway.example.com:443/mina/webrtc/signal");
1056    }
1057
1058    #[test]
1059    fn test_proxied_just_slash() {
1060        let method = SignalingMethod::Proxied(
1061            ProxyScheme::Https,
1062            "/".into(),
1063            HttpSignalingInfo {
1064                host: Host::Domain("gateway.example.com".to_string()),
1065                port: 443,
1066            },
1067        );
1068
1069        let url = method.http_url().unwrap();
1070        // Just "/" should work correctly
1071        assert_eq!(url, "https://gateway.example.com:443/mina/webrtc/signal");
1072    }
1073
1074    #[test]
1075    fn test_proxied_slash_variations() {
1076        let info = HttpSignalingInfo {
1077            host: Host::Domain("example.com".to_string()),
1078            port: 443,
1079        };
1080
1081        // No slashes
1082        let m1 = SignalingMethod::Proxied(ProxyScheme::Https, "path".into(), info.clone());
1083        assert_eq!(
1084            m1.http_url().unwrap(),
1085            "https://example.com:443/path/mina/webrtc/signal"
1086        );
1087
1088        // Leading slash only
1089        let m2 = SignalingMethod::Proxied(ProxyScheme::Https, "/path".into(), info.clone());
1090        assert_eq!(
1091            m2.http_url().unwrap(),
1092            "https://example.com:443/path/mina/webrtc/signal"
1093        );
1094
1095        // Trailing slash only
1096        let m3 = SignalingMethod::Proxied(ProxyScheme::Https, "path/".into(), info.clone());
1097        assert_eq!(
1098            m3.http_url().unwrap(),
1099            "https://example.com:443/path/mina/webrtc/signal"
1100        );
1101
1102        // Both slashes
1103        let m4 = SignalingMethod::Proxied(ProxyScheme::Https, "/path/".into(), info.clone());
1104        assert_eq!(
1105            m4.http_url().unwrap(),
1106            "https://example.com:443/path/mina/webrtc/signal"
1107        );
1108    }
1109
1110    #[test]
1111    fn test_proxied_multi_segment_path_slash_variations() {
1112        let info = HttpSignalingInfo {
1113            host: Host::Domain("example.com".to_string()),
1114            port: 443,
1115        };
1116
1117        // No outer slashes
1118        let m1 = SignalingMethod::Proxied(
1119            ProxyScheme::Https,
1120            "api/v2/clusters/123".into(),
1121            info.clone(),
1122        );
1123        assert_eq!(
1124            m1.http_url().unwrap(),
1125            "https://example.com:443/api/v2/clusters/123/mina/webrtc/signal"
1126        );
1127
1128        // Both outer slashes
1129        let m2 = SignalingMethod::Proxied(
1130            ProxyScheme::Https,
1131            "/api/v2/clusters/123/".into(),
1132            info.clone(),
1133        );
1134        assert_eq!(
1135            m2.http_url().unwrap(),
1136            "https://example.com:443/api/v2/clusters/123/mina/webrtc/signal"
1137        );
1138    }
1139
1140    #[test]
1141    fn test_proxied_roundtrip_just_slash() {
1142        // %2F is URL-encoded "/"
1143        let method: SignalingMethod = "/proxied/https/%2F/example.com/443".parse().unwrap();
1144        match &method {
1145            SignalingMethod::Proxied(ProxyScheme::Https, prefix, info) => {
1146                assert_eq!(prefix.as_ref(), "/");
1147                assert_eq!(info.host, Host::Domain("example.com".to_string()));
1148                assert_eq!(info.port, 443);
1149            }
1150            x => panic!("Expected Proxied variant, got {x:?}"),
1151        }
1152
1153        // Roundtrip
1154        let serialized = method.to_string();
1155        let deserialized: SignalingMethod = serialized.parse().unwrap();
1156        assert_eq!(method, deserialized);
1157    }
1158
1159    #[test]
1160    fn test_proxied_roundtrip_empty_prefix() {
1161        // Empty string prefix can't roundtrip because the parser filters empty components.
1162        // This is acceptable - empty prefix is treated as "no prefix" and produces
1163        // the same URL as Https variant. Test verifies the expected parse error.
1164        let original = SignalingMethod::Proxied(
1165            ProxyScheme::Https,
1166            "".into(),
1167            HttpSignalingInfo {
1168                host: Host::Domain("example.com".to_string()),
1169                port: 443,
1170            },
1171        );
1172
1173        let serialized = original.to_string();
1174        // Format is /proxied//example.com/443 - empty prefix component
1175        // Parser sees: ["proxied", "example.com", "443"] after filtering empties
1176        // This means it tries to parse "example.com" as the prefix, "443" as host
1177        let result: Result<SignalingMethod, _> = serialized.parse();
1178        assert!(
1179            result.is_err(),
1180            "Empty prefix can't roundtrip - use just '/' prefix instead"
1181        );
1182    }
1183
1184    // HTTP proxy tests (new functionality)
1185
1186    #[test]
1187    fn test_proxied_http_scheme() {
1188        let method = SignalingMethod::Proxied(
1189            ProxyScheme::Http,
1190            "/api/proxy".into(),
1191            HttpSignalingInfo {
1192                host: Host::Domain("gateway.example.com".to_string()),
1193                port: 8080,
1194            },
1195        );
1196
1197        let url = method.http_url().unwrap();
1198        assert_eq!(
1199            url,
1200            "http://gateway.example.com:8080/api/proxy/mina/webrtc/signal"
1201        );
1202    }
1203
1204    #[test]
1205    fn test_proxied_http_scheme_roundtrip() {
1206        let original = SignalingMethod::Proxied(
1207            ProxyScheme::Http,
1208            "/dev/proxy".into(),
1209            HttpSignalingInfo {
1210                host: Host::Domain("localhost".to_string()),
1211                port: 3000,
1212            },
1213        );
1214
1215        let serialized = original.to_string();
1216        assert!(serialized.contains("/proxied/http/"));
1217
1218        let deserialized: SignalingMethod = serialized.parse().unwrap();
1219        assert_eq!(original, deserialized);
1220    }
1221
1222    #[test]
1223    fn test_from_str_proxied_http() {
1224        let method: SignalingMethod = "/proxied/http/%2Fdev%2Fproxy/localhost/3000"
1225            .parse()
1226            .unwrap();
1227        match method {
1228            SignalingMethod::Proxied(ProxyScheme::Http, prefix, info) => {
1229                assert_eq!(prefix.as_ref(), "/dev/proxy");
1230                assert_eq!(info.host, Host::Domain("localhost".to_string()));
1231                assert_eq!(info.port, 3000);
1232            }
1233            x => panic!("Expected Proxied variant with Http scheme, got {x:?}"),
1234        }
1235    }
1236
1237    #[test]
1238    fn test_proxied_invalid_scheme() {
1239        let result: Result<SignalingMethod, _> = "/proxied/ftp/%2Fpath/example.com/21".parse();
1240        assert_eq!(
1241            result,
1242            Err(SignalingMethodParseError::UnknownSignalingMethod(
1243                "proxied/ftp".to_string()
1244            ))
1245        );
1246    }
1247}