1mod 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#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, derive_more::Display)]
71pub struct PathPrefix(String);
72
73impl PathPrefix {
74 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#[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 #[display(fmt = "http")]
129 Http,
130 #[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#[derive(BinProtWrite, BinProtRead, Eq, PartialEq, Ord, PartialOrd, Debug, Clone)]
183pub enum SignalingMethod {
184 Http(HttpSignalingInfo),
189
190 Https(HttpSignalingInfo),
195
196 HttpsProxy(u16, HttpSignalingInfo),
204
205 P2p {
211 relay_peer_id: PeerId,
213 },
214
215 Proxied(ProxyScheme, PathPrefix, HttpSignalingInfo),
225}
226
227impl SignalingMethod {
228 pub fn can_connect_directly(&self) -> bool {
242 !matches!(self, Self::P2p { .. })
243 }
244
245 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 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 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 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#[derive(Error, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
379pub enum SignalingMethodParseError {
380 #[error("not enough args for the signaling method")]
386 NotEnoughArgs,
387
388 #[error("unknown signaling method: `{0}`")]
393 UnknownSignalingMethod(String),
394
395 #[error("invalid cluster id")]
400 InvalidClusterId,
401
402 #[error("host parse error: {0}")]
407 HostParseError(String),
408
409 #[error("port parse error: {0}")]
414 PortParseError(String),
415}
416
417impl FromStr for SignalingMethod {
418 type Err = SignalingMethodParseError;
419
420 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 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 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 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 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 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 let result: Result<SignalingMethod, _> = "/http//8080".parse();
748 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 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 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 #[test]
889 fn test_from_str_valid_proxied() {
890 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 #[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}