tendermint_rpc/
rpc_url.rs

1//! URL representation for RPC clients.
2
3use core::{fmt, str::FromStr};
4
5use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::{error::Error, prelude::*};
8
9/// The various schemes supported by Tendermint RPC clients.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum Scheme {
12    Http,
13    Https,
14    WebSocket,
15    SecureWebSocket,
16}
17
18impl fmt::Display for Scheme {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Scheme::Http => write!(f, "http"),
22            Scheme::Https => write!(f, "https"),
23            Scheme::WebSocket => write!(f, "ws"),
24            Scheme::SecureWebSocket => write!(f, "wss"),
25        }
26    }
27}
28
29impl FromStr for Scheme {
30    type Err = crate::Error;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        Ok(match s {
34            "http" | "tcp" => Scheme::Http,
35            "https" => Scheme::Https,
36            "ws" => Scheme::WebSocket,
37            "wss" => Scheme::SecureWebSocket,
38            _ => return Err(Error::unsupported_scheme(s.to_string())),
39        })
40    }
41}
42
43/// A uniform resource locator (URL), with support for only those
44/// schemes/protocols supported by Tendermint RPC clients.
45///
46/// Re-implements relevant parts of [`url::Url`]'s interface with convenience
47/// mechanisms for transformation to/from other types.
48#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct Url {
50    inner: url::Url,
51    scheme: Scheme,
52}
53
54impl FromStr for Url {
55    type Err = Error;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        let url: url::Url = s.parse().map_err(Error::parse_url)?;
59        url.try_into()
60    }
61}
62
63impl Url {
64    /// Returns whether or not this URL represents a connection to a secure
65    /// endpoint.
66    pub fn is_secure(&self) -> bool {
67        match self.scheme {
68            Scheme::Http => false,
69            Scheme::Https => true,
70            Scheme::WebSocket => false,
71            Scheme::SecureWebSocket => true,
72        }
73    }
74
75    /// Get the scheme associated with this URL.
76    pub fn scheme(&self) -> Scheme {
77        self.scheme
78    }
79
80    /// Get the username associated with this URL, if any.
81    pub fn username(&self) -> Option<&str> {
82        Some(self.inner.username()).filter(|s| !s.is_empty())
83    }
84
85    /// Get the password associated with this URL, if any.
86    pub fn password(&self) -> Option<&str> {
87        self.inner.password()
88    }
89
90    /// Get the authority associated with this URL, if any.
91    /// The authority is the username and password separated by a colon.
92    pub fn authority(&self) -> Option<String> {
93        self.username()
94            .map(|user| format!("{}:{}", user, self.password().unwrap_or_default()))
95    }
96
97    /// Get the host associated with this URL.
98    pub fn host(&self) -> &str {
99        self.inner.host_str().unwrap()
100    }
101
102    /// Get the port associated with this URL.
103    pub fn port(&self) -> u16 {
104        self.inner.port_or_known_default().unwrap()
105    }
106
107    /// Get this URL's path.
108    pub fn path(&self) -> &str {
109        self.inner.path()
110    }
111}
112
113impl fmt::Display for Url {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(f, "{}", self.inner)
116    }
117}
118
119impl AsRef<url::Url> for Url {
120    fn as_ref(&self) -> &url::Url {
121        &self.inner
122    }
123}
124
125impl From<Url> for url::Url {
126    fn from(value: Url) -> Self {
127        value.inner
128    }
129}
130
131impl TryFrom<url::Url> for Url {
132    type Error = crate::Error;
133
134    fn try_from(url: url::Url) -> Result<Self, Self::Error> {
135        let scheme: Scheme = url.scheme().parse()?;
136
137        if url.host_str().is_none() {
138            return Err(Error::invalid_params(format!(
139                "URL is missing its host: {url}"
140            )));
141        }
142
143        if url.port_or_known_default().is_none() {
144            return Err(Error::invalid_params(format!(
145                "cannot determine appropriate port for URL: {url}"
146            )));
147        }
148
149        Ok(Self { inner: url, scheme })
150    }
151}
152
153impl Serialize for Url {
154    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155    where
156        S: Serializer,
157    {
158        self.to_string().serialize(serializer)
159    }
160}
161
162impl<'de> Deserialize<'de> for Url {
163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let s = String::deserialize(deserializer)?;
168        Url::from_str(&s).map_err(|e| D::Error::custom(e.to_string()))
169    }
170}
171
172#[cfg(test)]
173mod test {
174    use lazy_static::lazy_static;
175
176    use super::*;
177
178    struct ExpectedUrl {
179        scheme: Scheme,
180        host: String,
181        port: u16,
182        path: String,
183        username: Option<String>,
184        password: Option<String>,
185    }
186
187    lazy_static! {
188        static ref SUPPORTED_URLS: Vec<(String, ExpectedUrl)> = vec![
189            (
190                "tcp://127.0.0.1:26657".to_owned(),
191                ExpectedUrl {
192                    scheme: Scheme::Http,
193                    host: "127.0.0.1".to_string(),
194                    port: 26657,
195                    path: "".to_string(),
196                    username: None,
197                    password: None,
198                }
199            ),
200            (
201                "tcp://foo@127.0.0.1:26657".to_owned(),
202                ExpectedUrl {
203                    scheme: Scheme::Http,
204                    host: "127.0.0.1".to_string(),
205                    port: 26657,
206                    path: "".to_string(),
207                    username: Some("foo".to_string()),
208                    password: None,
209                }
210            ),
211            (
212                "tcp://foo:bar@127.0.0.1:26657".to_owned(),
213                ExpectedUrl {
214                    scheme: Scheme::Http,
215                    host: "127.0.0.1".to_string(),
216                    port: 26657,
217                    path: "".to_string(),
218                    username: Some("foo".to_string()),
219                    password: Some("bar".to_string()),
220                }
221            ),
222            (
223                "http://127.0.0.1:26657".to_owned(),
224                ExpectedUrl {
225                    scheme: Scheme::Http,
226                    host: "127.0.0.1".to_string(),
227                    port: 26657,
228                    path: "/".to_string(),
229                    username: None,
230                    password: None,
231                }
232            ),
233            (
234                "http://foo@127.0.0.1:26657".to_owned(),
235                ExpectedUrl {
236                    scheme: Scheme::Http,
237                    host: "127.0.0.1".to_string(),
238                    port: 26657,
239                    path: "/".to_string(),
240                    username: Some("foo".to_string()),
241                    password: None,
242                }
243            ),
244            (
245                "http://foo:bar@127.0.0.1:26657".to_owned(),
246                ExpectedUrl {
247                    scheme: Scheme::Http,
248                    host: "127.0.0.1".to_string(),
249                    port: 26657,
250                    path: "/".to_string(),
251                    username: Some("foo".to_string()),
252                    password: Some("bar".to_string()),
253                }
254            ),
255            (
256                "https://127.0.0.1:26657".to_owned(),
257                ExpectedUrl {
258                    scheme: Scheme::Https,
259                    host: "127.0.0.1".to_string(),
260                    port: 26657,
261                    path: "/".to_string(),
262                    username: None,
263                    password: None,
264                }
265            ),
266            (
267                "https://foo@127.0.0.1:26657".to_owned(),
268                ExpectedUrl {
269                    scheme: Scheme::Https,
270                    host: "127.0.0.1".to_string(),
271                    port: 26657,
272                    path: "/".to_string(),
273                    username: Some("foo".to_string()),
274                    password: None,
275                }
276            ),
277            (
278                "https://foo:bar@127.0.0.1:26657".to_owned(),
279                ExpectedUrl {
280                    scheme: Scheme::Https,
281                    host: "127.0.0.1".to_string(),
282                    port: 26657,
283                    path: "/".to_string(),
284                    username: Some("foo".to_string()),
285                    password: Some("bar".to_string()),
286                }
287            ),
288            (
289                "ws://127.0.0.1:26657/websocket".to_owned(),
290                ExpectedUrl {
291                    scheme: Scheme::WebSocket,
292                    host: "127.0.0.1".to_string(),
293                    port: 26657,
294                    path: "/websocket".to_string(),
295                    username: None,
296                    password: None,
297                }
298            ),
299            (
300                "ws://foo@127.0.0.1:26657/websocket".to_owned(),
301                ExpectedUrl {
302                    scheme: Scheme::WebSocket,
303                    host: "127.0.0.1".to_string(),
304                    port: 26657,
305                    path: "/websocket".to_string(),
306                    username: Some("foo".to_string()),
307                    password: None,
308                }
309            ),
310            (
311                "ws://foo:bar@127.0.0.1:26657/websocket".to_owned(),
312                ExpectedUrl {
313                    scheme: Scheme::WebSocket,
314                    host: "127.0.0.1".to_string(),
315                    port: 26657,
316                    path: "/websocket".to_string(),
317                    username: Some("foo".to_string()),
318                    password: Some("bar".to_string()),
319                }
320            ),
321            (
322                "wss://127.0.0.1:26657/websocket".to_owned(),
323                ExpectedUrl {
324                    scheme: Scheme::SecureWebSocket,
325                    host: "127.0.0.1".to_string(),
326                    port: 26657,
327                    path: "/websocket".to_string(),
328                    username: None,
329                    password: None,
330                }
331            ),
332            (
333                "wss://foo@127.0.0.1:26657/websocket".to_owned(),
334                ExpectedUrl {
335                    scheme: Scheme::SecureWebSocket,
336                    host: "127.0.0.1".to_string(),
337                    port: 26657,
338                    path: "/websocket".to_string(),
339                    username: Some("foo".to_string()),
340                    password: None,
341                }
342            ),
343            (
344                "wss://foo:bar@127.0.0.1:26657/websocket".to_owned(),
345                ExpectedUrl {
346                    scheme: Scheme::SecureWebSocket,
347                    host: "127.0.0.1".to_string(),
348                    port: 26657,
349                    path: "/websocket".to_string(),
350                    username: Some("foo".to_string()),
351                    password: Some("bar".to_string()),
352                }
353            )
354        ];
355    }
356
357    #[test]
358    fn parsing() {
359        for (url_str, expected) in SUPPORTED_URLS.iter() {
360            let u = Url::from_str(url_str).unwrap();
361            assert_eq!(expected.scheme, u.scheme(), "{url_str}");
362            assert_eq!(expected.host, u.host(), "{url_str}");
363            assert_eq!(expected.port, u.port(), "{url_str}");
364            assert_eq!(expected.path, u.path(), "{url_str}");
365            if let Some(n) = u.username() {
366                assert_eq!(expected.username.as_ref().unwrap(), n, "{url_str}");
367            } else {
368                assert!(expected.username.is_none(), "{}", url_str);
369            }
370            if let Some(pw) = u.password() {
371                assert_eq!(expected.password.as_ref().unwrap(), pw, "{url_str}");
372            } else {
373                assert!(expected.password.is_none(), "{}", url_str);
374            }
375        }
376    }
377}