Skip to main content

torrust_net_primitives/
service_binding.rs

1use std::fmt;
2use std::net::{IpAddr, SocketAddr};
3
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7const DUAL_STACK_IP_V4_MAPPED_V6_PREFIX: &str = "::ffff:";
8
9/// Represents the supported network protocols.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
11pub enum Protocol {
12    UDP,
13    HTTP,
14    HTTPS,
15}
16
17impl fmt::Display for Protocol {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        let proto_str = match self {
20            Protocol::UDP => "udp",
21            Protocol::HTTP => "http",
22            Protocol::HTTPS => "https",
23        };
24        write!(f, "{proto_str}")
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
29pub enum IpType {
30    /// Represents a plain IPv4 or IPv6 address.
31    Plain,
32
33    /// Represents an IPv6 address that is a mapped IPv4 address.
34    ///
35    /// This is used for IPv6 addresses that represent an IPv4 address in a dual-stack network.
36    ///
37    /// For example: `[::ffff:192.0.2.33]`
38    V4MappedV6,
39}
40
41impl fmt::Display for IpType {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        let ip_type_str = match self {
44            Self::Plain => "plain",
45            Self::V4MappedV6 => "v4_mapped_v6",
46        };
47        write!(f, "{ip_type_str}")
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
52pub enum IpFamily {
53    // IPv4
54    Inet,
55    // IPv6
56    Inet6,
57}
58
59impl fmt::Display for IpFamily {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        let ip_family_str = match self {
62            Self::Inet => "inet",
63            Self::Inet6 => "inet6",
64        };
65        write!(f, "{ip_family_str}")
66    }
67}
68
69impl From<IpAddr> for IpFamily {
70    fn from(ip: IpAddr) -> Self {
71        if ip.is_ipv4() {
72            return IpFamily::Inet;
73        }
74
75        if ip.is_ipv6() {
76            return IpFamily::Inet6;
77        }
78
79        panic!("Unsupported IP address type: {ip}");
80    }
81}
82
83#[derive(thiserror::Error, Debug, Clone)]
84pub enum Error {
85    #[error("The port number cannot be zero. It must be an assigned valid port.")]
86    PortZeroNotAllowed,
87}
88
89/// Represents a network service binding, encapsulating protocol and socket
90/// address.
91///
92/// This struct is used to define how a service binds to a network interface and
93/// port.
94///
95/// It's an URL without path and some restrictions:
96///
97/// - Only some schemes are accepted: `udp`, `http`, `https`.
98/// - The port number must be greater than zero. The service should be already
99///   listening on that port.
100/// - The authority part of the URL must be a valid socket address (wildcard is
101///   accepted).
102///
103/// Besides it accepts some non well-formed URLs, like:<http://127.0.0.1:7070>
104/// or <https://127.0.0.1:7070>. Those URLs are not valid because they use non
105/// standard ports (80 and 443).
106///
107/// NOTICE: It does not represent a public valid URL clients can connect to. It
108/// represents the service's internal URL configuration after assigning a port.
109/// If the port in the configuration is not zero, it's basically the same
110/// information you get from the configuration (binding address + protocol).
111///
112/// # Examples
113///
114/// ```
115/// use std::net::{IpAddr, Ipv4Addr, SocketAddr};
116/// use torrust_net_primitives::service_binding::{ServiceBinding, Protocol};
117///
118/// let service_binding = ServiceBinding::new(Protocol::HTTP, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 7070)).unwrap();
119///
120/// assert_eq!(service_binding.url().to_string(), "http://127.0.0.1:7070/".to_string());
121/// ```
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
123pub struct ServiceBinding {
124    /// The network protocol used by the service (UDP, HTTP, HTTPS).
125    protocol: Protocol,
126
127    /// The socket address (IP and port) to which the service binds.
128    bind_address: SocketAddr,
129}
130
131impl ServiceBinding {
132    /// # Errors
133    ///
134    /// This function will return an error if the port number is zero.
135    pub fn new(protocol: Protocol, bind_address: SocketAddr) -> Result<Self, Error> {
136        if bind_address.port() == 0 {
137            return Err(Error::PortZeroNotAllowed);
138        }
139
140        Ok(Self { protocol, bind_address })
141    }
142
143    /// Returns the protocol used by the service.
144    #[must_use]
145    pub fn protocol(&self) -> Protocol {
146        self.protocol.clone()
147    }
148
149    #[must_use]
150    pub fn bind_address(&self) -> SocketAddr {
151        self.bind_address
152    }
153
154    #[must_use]
155    pub fn bind_address_ip_type(&self) -> IpType {
156        if self.is_v4_mapped_v6() {
157            return IpType::V4MappedV6;
158        }
159
160        IpType::Plain
161    }
162
163    #[must_use]
164    pub fn bind_address_ip_family(&self) -> IpFamily {
165        self.bind_address.ip().into()
166    }
167
168    /// # Panics
169    ///
170    /// It never panics because the URL is always valid.
171    #[must_use]
172    pub fn url(&self) -> Url {
173        Url::parse(&format!("{}://{}", self.protocol, self.bind_address))
174            .expect("Service binding can always be parsed into a URL")
175    }
176
177    fn is_v4_mapped_v6(&self) -> bool {
178        self.bind_address.ip().is_ipv6()
179            && self
180                .bind_address
181                .ip()
182                .to_string()
183                .starts_with(DUAL_STACK_IP_V4_MAPPED_V6_PREFIX)
184    }
185}
186
187impl From<ServiceBinding> for Url {
188    fn from(service_binding: ServiceBinding) -> Self {
189        service_binding.url()
190    }
191}
192
193impl fmt::Display for ServiceBinding {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{}", self.url())
196    }
197}
198
199#[cfg(test)]
200mod tests {
201
202    mod the_service_binding {
203        use std::net::SocketAddr;
204        use std::str::FromStr;
205
206        use rstest::rstest;
207        use url::Url;
208
209        use crate::service_binding::{Error, IpType, Protocol, ServiceBinding};
210
211        #[rstest]
212        #[case("wildcard_ip", Protocol::UDP, SocketAddr::from_str("0.0.0.0:6969").unwrap())]
213        #[case("udp_service", Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap())]
214        #[case("http_service", Protocol::HTTP, SocketAddr::from_str("127.0.0.1:7070").unwrap())]
215        #[case("https_service", Protocol::HTTPS, SocketAddr::from_str("127.0.0.1:7070").unwrap())]
216        fn should_allow_a_subset_of_urls(#[case] case: &str, #[case] protocol: Protocol, #[case] bind_address: SocketAddr) {
217            let service_binding = ServiceBinding::new(protocol.clone(), bind_address);
218
219            assert!(service_binding.is_ok(), "{}", format!("{case} failed: {service_binding:?}"));
220        }
221
222        #[test]
223        fn should_not_allow_undefined_port_zero() {
224            let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:0").unwrap());
225
226            assert!(matches!(service_binding, Err(Error::PortZeroNotAllowed)));
227        }
228
229        #[test]
230        fn should_return_the_bind_address() {
231            let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
232
233            assert_eq!(
234                service_binding.bind_address(),
235                SocketAddr::from_str("127.0.0.1:6969").unwrap()
236            );
237        }
238
239        #[test]
240        fn should_return_the_bind_address_plain_type_for_ipv4_ips() {
241            let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
242
243            assert_eq!(service_binding.bind_address_ip_type(), IpType::Plain);
244        }
245
246        #[test]
247        fn should_return_the_bind_address_plain_type_for_ipv6_ips() {
248            let service_binding =
249                ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("[0:0:0:0:0:0:0:1]:6969").unwrap()).unwrap();
250
251            assert_eq!(service_binding.bind_address_ip_type(), IpType::Plain);
252        }
253
254        #[test]
255        fn should_return_the_bind_address_v4_mapped_v7_type_for_ipv4_ips_mapped_to_ipv6() {
256            let service_binding =
257                ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("[::ffff:192.0.2.33]:6969").unwrap()).unwrap();
258
259            assert_eq!(service_binding.bind_address_ip_type(), IpType::V4MappedV6);
260        }
261
262        #[test]
263        fn should_return_the_corresponding_url() {
264            let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
265
266            assert_eq!(service_binding.url(), Url::parse("udp://127.0.0.1:6969").unwrap());
267        }
268
269        #[test]
270        fn should_be_converted_into_an_url() {
271            let service_binding = ServiceBinding::new(Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap()).unwrap();
272
273            let url: Url = service_binding.clone().into();
274
275            assert_eq!(url, Url::parse("udp://127.0.0.1:6969").unwrap());
276        }
277
278        #[rstest]
279        #[case("udp_service", Protocol::UDP, SocketAddr::from_str("127.0.0.1:6969").unwrap(), "udp://127.0.0.1:6969")]
280        #[case("http_service", Protocol::HTTP, SocketAddr::from_str("127.0.0.1:7070").unwrap(), "http://127.0.0.1:7070/")]
281        #[case("https_service", Protocol::HTTPS, SocketAddr::from_str("127.0.0.1:7070").unwrap(), "https://127.0.0.1:7070/")]
282        fn should_always_have_a_corresponding_unique_url(
283            #[case] case: &str,
284            #[case] protocol: Protocol,
285            #[case] bind_address: SocketAddr,
286            #[case] expected_url: String,
287        ) {
288            let service_binding = ServiceBinding::new(protocol.clone(), bind_address).unwrap();
289
290            assert_eq!(
291                service_binding.url().to_string(),
292                expected_url,
293                "{case} failed: {service_binding:?}",
294            );
295        }
296    }
297}