atm0s_sdn_multiaddr/
from_url.rs

1use crate::{Multiaddr, Protocol};
2use std::{error, fmt, iter, net::IpAddr};
3
4/// Attempts to parse an URL into a multiaddress.
5///
6/// This function will return an error if some information in the URL cannot be retained in the
7/// generated multiaddress. This includes a username, password, path (if not supported by the
8/// multiaddr), and query string.
9///
10/// This function is only present if the `url` feature is enabled, and it is
11/// enabled by default.
12///
13/// The supported URL schemes are:
14///
15/// - `ws://example.com/`
16/// - `wss://example.com/`
17/// - `http://example.com/`
18/// - `https://example.com/`
19/// - `unix:/foo/bar`
20///
21/// # Example
22///
23/// ```
24/// let addr = atm0s_sdn_multiaddr::from_url("ws://127.0.0.1:8080/").unwrap();
25/// assert_eq!(addr, "/ip4/127.0.0.1/tcp/8080/ws".parse().unwrap());
26/// ```
27///
28pub fn from_url(url: &str) -> std::result::Result<Multiaddr, FromUrlErr> {
29    from_url_inner(url, false)
30}
31
32/// Attempts to parse an URL into a multiaddress. Ignores possible loss of information.
33///
34/// This function is similar to [`from_url`], except that we don't return an error if some
35/// information in the URL cannot be retain in the generated multiaddres.
36///
37/// This function is only present if the `url` feature is enabled, and it is
38/// enabled by default.
39///
40/// # Example
41///
42/// ```
43/// let addr = "ws://user:pass@127.0.0.1:8080/";
44/// assert!(atm0s_sdn_multiaddr::from_url(addr).is_err());
45/// assert!(atm0s_sdn_multiaddr::from_url_lossy(addr).is_ok());
46/// ```
47///
48pub fn from_url_lossy(url: &str) -> std::result::Result<Multiaddr, FromUrlErr> {
49    from_url_inner(url, true)
50}
51
52/// Underlying implementation of `from_url` and `from_url_lossy`.
53fn from_url_inner(url: &str, lossy: bool) -> std::result::Result<Multiaddr, FromUrlErr> {
54    let url = url::Url::parse(url).map_err(|_| FromUrlErr::BadUrl)?;
55
56    match url.scheme() {
57        // Note: if you add support for a new scheme, please update the documentation as well.
58        "ws" | "wss" | "http" | "https" => from_url_inner_http_ws(url, lossy),
59        "unix" => from_url_inner_path(url, lossy),
60        _ => Err(FromUrlErr::UnsupportedScheme),
61    }
62}
63
64/// Called when `url.scheme()` is an Internet-like URL.
65fn from_url_inner_http_ws(url: url::Url, lossy: bool) -> std::result::Result<Multiaddr, FromUrlErr> {
66    let (protocol, lost_path, default_port) = match url.scheme() {
67        "ws" => (Protocol::Ws(url.path().to_owned().into()), false, 80),
68        "wss" => (Protocol::Wss(url.path().to_owned().into()), false, 443),
69        "http" => (Protocol::Http, true, 80),
70        "https" => (Protocol::Https, true, 443),
71        _ => unreachable!("We only call this function for one of the given schemes; qed"),
72    };
73
74    let port = Protocol::Tcp(url.port().unwrap_or(default_port));
75    let ip = if let Some(hostname) = url.host_str() {
76        if let Ok(ip) = hostname.parse::<IpAddr>() {
77            Protocol::from(ip)
78        } else {
79            Protocol::Dns(hostname.into())
80        }
81    } else {
82        return Err(FromUrlErr::BadUrl);
83    };
84
85    if !lossy && (!url.username().is_empty() || url.password().is_some() || (lost_path && url.path() != "/" && !url.path().is_empty()) || url.query().is_some() || url.fragment().is_some()) {
86        return Err(FromUrlErr::InformationLoss);
87    }
88
89    Ok(iter::once(ip).chain(iter::once(port)).chain(iter::once(protocol)).collect())
90}
91
92/// Called when `url.scheme()` is a path-like URL.
93fn from_url_inner_path(url: url::Url, lossy: bool) -> std::result::Result<Multiaddr, FromUrlErr> {
94    let protocol = match url.scheme() {
95        "unix" => Protocol::Unix(url.path().to_owned().into()),
96        _ => unreachable!("We only call this function for one of the given schemes; qed"),
97    };
98
99    if !lossy && (!url.username().is_empty() || url.password().is_some() || url.query().is_some() || url.fragment().is_some()) {
100        return Err(FromUrlErr::InformationLoss);
101    }
102
103    Ok(Multiaddr::from(protocol))
104}
105
106/// Error while parsing an URL.
107#[derive(Debug)]
108pub enum FromUrlErr {
109    /// Failed to parse the URL.
110    BadUrl,
111    /// The URL scheme was not recognized.
112    UnsupportedScheme,
113    /// Some information in the URL would be lost. Never returned by `from_url_lossy`.
114    InformationLoss,
115}
116
117impl fmt::Display for FromUrlErr {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            FromUrlErr::BadUrl => write!(f, "Bad URL"),
121            FromUrlErr::UnsupportedScheme => write!(f, "Unrecognized URL scheme"),
122            FromUrlErr::InformationLoss => write!(f, "Some information in the URL would be lost"),
123        }
124    }
125}
126
127impl error::Error for FromUrlErr {}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn parse_garbage_doesnt_panic() {
135        for _ in 0..50 {
136            let url = (0..16).map(|_| rand::random::<u8>()).collect::<Vec<_>>();
137            let url = String::from_utf8_lossy(&url);
138            assert!(from_url(&url).is_err());
139        }
140    }
141
142    #[test]
143    fn normal_usage_ws() {
144        let addr = from_url("ws://127.0.0.1:8000").unwrap();
145        assert_eq!(addr, "/ip4/127.0.0.1/tcp/8000/ws".parse().unwrap());
146    }
147
148    #[test]
149    fn normal_usage_wss() {
150        let addr = from_url("wss://127.0.0.1:8000").unwrap();
151        assert_eq!(addr, "/ip4/127.0.0.1/tcp/8000/wss".parse().unwrap());
152    }
153
154    #[test]
155    fn default_ws_port() {
156        let addr = from_url("ws://127.0.0.1").unwrap();
157        assert_eq!(addr, "/ip4/127.0.0.1/tcp/80/ws".parse().unwrap());
158    }
159
160    #[test]
161    fn default_http_port() {
162        let addr = from_url("http://127.0.0.1").unwrap();
163        assert_eq!(addr, "/ip4/127.0.0.1/tcp/80/http".parse().unwrap());
164    }
165
166    #[test]
167    fn default_wss_port() {
168        let addr = from_url("wss://127.0.0.1").unwrap();
169        assert_eq!(addr, "/ip4/127.0.0.1/tcp/443/wss".parse().unwrap());
170    }
171
172    #[test]
173    fn default_https_port() {
174        let addr = from_url("https://127.0.0.1").unwrap();
175        assert_eq!(addr, "/ip4/127.0.0.1/tcp/443/https".parse().unwrap());
176    }
177
178    #[test]
179    fn dns_addr_ws() {
180        let addr = from_url("ws://example.com").unwrap();
181        assert_eq!(addr, "/dns/example.com/tcp/80/ws".parse().unwrap());
182    }
183
184    #[test]
185    fn dns_addr_http() {
186        let addr = from_url("http://example.com").unwrap();
187        assert_eq!(addr, "/dns/example.com/tcp/80/http".parse().unwrap());
188    }
189
190    #[test]
191    fn dns_addr_wss() {
192        let addr = from_url("wss://example.com").unwrap();
193        assert_eq!(addr, "/dns/example.com/tcp/443/wss".parse().unwrap());
194    }
195
196    #[test]
197    fn dns_addr_https() {
198        let addr = from_url("https://example.com").unwrap();
199        assert_eq!(addr, "/dns/example.com/tcp/443/https".parse().unwrap());
200    }
201
202    #[test]
203    fn bad_hostname() {
204        let addr = from_url("wss://127.0.0.1x").unwrap();
205        assert_eq!(addr, "/dns/127.0.0.1x/tcp/443/wss".parse().unwrap());
206    }
207
208    #[test]
209    fn wrong_scheme() {
210        match from_url("foo://127.0.0.1") {
211            Err(FromUrlErr::UnsupportedScheme) => {}
212            _ => panic!(),
213        }
214    }
215
216    #[test]
217    fn dns_and_port() {
218        let addr = from_url("http://example.com:1000").unwrap();
219        assert_eq!(addr, "/dns/example.com/tcp/1000/http".parse().unwrap());
220    }
221
222    #[test]
223    fn username_lossy() {
224        let addr = "http://foo@example.com:1000/";
225        assert!(from_url(addr).is_err());
226        assert!(from_url_lossy(addr).is_ok());
227        assert!(from_url("http://@example.com:1000/").is_ok());
228    }
229
230    #[test]
231    fn password_lossy() {
232        let addr = "http://:bar@example.com:1000/";
233        assert!(from_url(addr).is_err());
234        assert!(from_url_lossy(addr).is_ok());
235    }
236
237    #[test]
238    fn path_lossy() {
239        let addr = "http://example.com:1000/foo";
240        assert!(from_url(addr).is_err());
241        assert!(from_url_lossy(addr).is_ok());
242    }
243
244    #[test]
245    fn fragment_lossy() {
246        let addr = "http://example.com:1000/#foo";
247        assert!(from_url(addr).is_err());
248        assert!(from_url_lossy(addr).is_ok());
249    }
250
251    #[test]
252    fn unix() {
253        let addr = from_url("unix:/foo/bar").unwrap();
254        assert_eq!(addr, Multiaddr::from(Protocol::Unix("/foo/bar".into())));
255    }
256
257    #[test]
258    fn ws_path() {
259        let addr = from_url("ws://1.2.3.4:1000/foo/bar").unwrap();
260        assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/x-parity-ws/%2ffoo%2fbar".parse().unwrap());
261
262        let addr = from_url("ws://1.2.3.4:1000/").unwrap();
263        assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/ws".parse().unwrap());
264
265        let addr = from_url("wss://1.2.3.4:1000/foo/bar").unwrap();
266        assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/x-parity-wss/%2ffoo%2fbar".parse().unwrap());
267
268        let addr = from_url("wss://1.2.3.4:1000").unwrap();
269        assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/wss".parse().unwrap());
270    }
271}