cometbft_config/
net.rs

1//! Remote addresses (`tcp://` or `unix://`)
2
3use core::{
4    fmt::{self, Display},
5    str::{self, FromStr},
6};
7
8use cometbft::node::{self, info::ListenAddress};
9use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
10use url::Url;
11
12use crate::{error::Error, prelude::*};
13
14/// URI prefix for TCP connections
15pub const TCP_PREFIX: &str = "tcp://";
16
17/// URI prefix for Unix socket connections
18pub const UNIX_PREFIX: &str = "unix://";
19
20/// Remote address (TCP or UNIX socket)
21///
22/// For TCP-based addresses, this supports both IPv4 and IPv6 addresses and
23/// hostnames.
24///
25/// If the scheme is not supplied (i.e. `tcp://` or `unix://`) when parsing
26/// from a string, it is assumed to be a TCP address.
27#[derive(Clone, Debug, Eq, Hash, PartialEq)]
28pub enum Address {
29    /// TCP connections
30    Tcp {
31        /// Remote peer ID
32        peer_id: Option<node::Id>,
33
34        /// Hostname or IP address
35        host: String,
36
37        /// Port
38        port: u16,
39    },
40
41    /// UNIX domain sockets
42    Unix {
43        /// Path to a UNIX domain socket path
44        path: String,
45    },
46}
47
48impl Address {
49    /// Convert `ListenAddress` to a `net::Address`
50    pub fn from_listen_address(address: &ListenAddress) -> Option<Self> {
51        let raw_address = address.as_str();
52        // TODO(tarcieri): validate these and handle them better at parse time
53        if raw_address.starts_with("tcp://") {
54            raw_address.parse().ok()
55        } else {
56            format!("tcp://{raw_address}").parse().ok()
57        }
58    }
59}
60
61impl<'de> Deserialize<'de> for Address {
62    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
63        Self::from_str(&String::deserialize(deserializer)?)
64            .map_err(|e| D::Error::custom(format!("{e}")))
65    }
66}
67
68impl Display for Address {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Address::Tcp {
72                peer_id: None,
73                host,
74                port,
75            } => write!(f, "{TCP_PREFIX}{host}:{port}"),
76            Address::Tcp {
77                peer_id: Some(peer_id),
78                host,
79                port,
80            } => write!(f, "{TCP_PREFIX}{peer_id}@{host}:{port}"),
81            Address::Unix { path } => write!(f, "{UNIX_PREFIX}{path}"),
82        }
83    }
84}
85
86impl FromStr for Address {
87    type Err = Error;
88
89    fn from_str(addr: &str) -> Result<Self, Error> {
90        // unix abstract socket address, `man 7 unix` for system descriptions
91        if addr.starts_with("unix://@") {
92            return Ok(Self::Unix {
93                path: addr.strip_prefix("unix://").unwrap().to_owned(),
94            });
95        }
96
97        let prefixed_addr = if addr.contains("://") {
98            addr.to_owned()
99        } else {
100            // If the address has no scheme, assume it's TCP
101            format!("{TCP_PREFIX}{addr}")
102        };
103        let url = Url::parse(&prefixed_addr).map_err(Error::parse_url)?;
104        match url.scheme() {
105            "tcp" => Ok(Self::Tcp {
106                peer_id: if !url.username().is_empty() {
107                    let username = url.username().parse().map_err(Error::cometbft)?;
108                    Some(username)
109                } else {
110                    None
111                },
112                host: url
113                    .host_str()
114                    .ok_or_else(|| {
115                        Error::parse(format!("invalid TCP address (missing host): {addr}"))
116                    })?
117                    .to_owned(),
118                port: url.port().ok_or_else(|| {
119                    Error::parse(format!("invalid TCP address (missing port): {addr}"))
120                })?,
121            }),
122            "unix" => Ok(Self::Unix {
123                path: url.path().to_string(),
124            }),
125            _ => Err(Error::parse(format!("invalid address scheme: {addr:?}"))),
126        }
127    }
128}
129
130impl Serialize for Address {
131    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
132        self.to_string().serialize(serializer)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use cometbft::node;
139
140    use super::*;
141
142    const EXAMPLE_TCP_ADDR: &str =
143        "tcp://abd636b766dcefb5322d8ca40011ec2cb35efbc2@35.192.61.41:26656";
144    const EXAMPLE_TCP_ADDR_WITHOUT_ID: &str = "tcp://35.192.61.41:26656";
145    const EXAMPLE_UNIX_ADDR: &str = "unix:///tmp/node.sock";
146    const EXAMPLE_TCP_IPV6_ADDR: &str =
147        "tcp://abd636b766dcefb5322d8ca40011ec2cb35efbc2@[2001:0000:3238:DFE1:0063:0000:0000:FEFB]:26656";
148
149    #[test]
150    fn parse_tcp_addr() {
151        let tcp_addr_without_prefix = &EXAMPLE_TCP_ADDR[TCP_PREFIX.len()..];
152
153        for tcp_addr in &[EXAMPLE_TCP_ADDR, tcp_addr_without_prefix] {
154            match tcp_addr.parse::<Address>().unwrap() {
155                Address::Tcp {
156                    peer_id,
157                    host,
158                    port,
159                } => {
160                    assert_eq!(
161                        peer_id.unwrap(),
162                        "abd636b766dcefb5322d8ca40011ec2cb35efbc2"
163                            .parse::<node::Id>()
164                            .unwrap()
165                    );
166                    assert_eq!(host, "35.192.61.41");
167                    assert_eq!(port, 26656);
168                },
169                other => panic!("unexpected address type: {other:?}"),
170            }
171        }
172    }
173
174    #[test]
175    fn parse_tcp_addr_without_id() {
176        let addr = EXAMPLE_TCP_ADDR_WITHOUT_ID.parse::<Address>().unwrap();
177        let addr_without_prefix = EXAMPLE_TCP_ADDR_WITHOUT_ID[TCP_PREFIX.len()..]
178            .parse::<Address>()
179            .unwrap();
180        for addr in &[addr, addr_without_prefix] {
181            match addr {
182                Address::Tcp {
183                    peer_id,
184                    host,
185                    port,
186                } => {
187                    assert!(peer_id.is_none());
188                    assert_eq!(host, "35.192.61.41");
189                    assert_eq!(*port, 26656);
190                },
191                other => panic!("unexpected address type: {other:?}"),
192            }
193        }
194    }
195
196    #[test]
197    fn parse_unix_addr() {
198        let addr = EXAMPLE_UNIX_ADDR.parse::<Address>().unwrap();
199        match addr {
200            Address::Unix { path } => {
201                assert_eq!(path, "/tmp/node.sock");
202            },
203            other => panic!("unexpected address type: {other:?}"),
204        }
205    }
206
207    #[test]
208    fn parse_tcp_ipv6_addr() {
209        let addr = EXAMPLE_TCP_IPV6_ADDR.parse::<Address>().unwrap();
210        let addr_without_prefix = EXAMPLE_TCP_IPV6_ADDR[TCP_PREFIX.len()..]
211            .parse::<Address>()
212            .unwrap();
213        for addr in &[addr, addr_without_prefix] {
214            match addr {
215                Address::Tcp {
216                    peer_id,
217                    host,
218                    port,
219                } => {
220                    assert_eq!(
221                        peer_id.unwrap(),
222                        "abd636b766dcefb5322d8ca40011ec2cb35efbc2"
223                            .parse::<node::Id>()
224                            .unwrap()
225                    );
226                    // The parser URL strips the leading zeroes and converts to lowercase hex
227                    assert_eq!(host, "[2001:0:3238:dfe1:63::fefb]");
228                    assert_eq!(*port, 26656);
229                },
230                other => panic!("unexpected address type: {other:?}"),
231            }
232        }
233    }
234}