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 = 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!(multiaddr::from_url(addr).is_err());
45/// assert!(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(
66    url: url::Url,
67    lossy: bool,
68) -> std::result::Result<Multiaddr, FromUrlErr> {
69    let (protocol, lost_path, default_port) = match url.scheme() {
70        "ws" => (Protocol::Ws(url.path().to_owned().into()), false, 80),
71        "wss" => (Protocol::Wss(url.path().to_owned().into()), false, 443),
72        "http" => (Protocol::Http, true, 80),
73        "https" => (Protocol::Https, true, 443),
74        _ => unreachable!("We only call this function for one of the given schemes; qed"),
75    };
76
77    let port = Protocol::Tcp(url.port().unwrap_or(default_port));
78    let ip = if let Some(hostname) = url.host_str() {
79        if let Ok(ip) = hostname.parse::<IpAddr>() {
80            Protocol::from(ip)
81        } else {
82            Protocol::Dns(hostname.into())
83        }
84    } else {
85        return Err(FromUrlErr::BadUrl);
86    };
87
88    if !lossy
89        && (!url.username().is_empty()
90            || url.password().is_some()
91            || (lost_path && url.path() != "/" && !url.path().is_empty())
92            || url.query().is_some()
93            || url.fragment().is_some())
94    {
95        return Err(FromUrlErr::InformationLoss);
96    }
97
98    Ok(iter::once(ip)
99        .chain(iter::once(port))
100        .chain(iter::once(protocol))
101        .collect())
102}
103
104/// Called when `url.scheme()` is a path-like URL.
105fn from_url_inner_path(url: url::Url, lossy: bool) -> std::result::Result<Multiaddr, FromUrlErr> {
106    let protocol = match url.scheme() {
107        "unix" => Protocol::Unix(url.path().to_owned().into()),
108        _ => unreachable!("We only call this function for one of the given schemes; qed"),
109    };
110
111    if !lossy
112        && (!url.username().is_empty()
113            || url.password().is_some()
114            || url.query().is_some()
115            || url.fragment().is_some())
116    {
117        return Err(FromUrlErr::InformationLoss);
118    }
119
120    Ok(Multiaddr::from(protocol))
121}
122
123/// Error while parsing an URL.
124#[derive(Debug)]
125pub enum FromUrlErr {
126    /// Failed to parse the URL.
127    BadUrl,
128    /// The URL scheme was not recognized.
129    UnsupportedScheme,
130    /// Some information in the URL would be lost. Never returned by `from_url_lossy`.
131    InformationLoss,
132}
133
134impl fmt::Display for FromUrlErr {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            FromUrlErr::BadUrl => write!(f, "Bad URL"),
138            FromUrlErr::UnsupportedScheme => write!(f, "Unrecognized URL scheme"),
139            FromUrlErr::InformationLoss => write!(f, "Some information in the URL would be lost"),
140        }
141    }
142}
143
144impl error::Error for FromUrlErr {}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn parse_garbage_doesnt_panic() {
152        for _ in 0..50 {
153            let url = (0..16).map(|_| rand::random::<u8>()).collect::<Vec<_>>();
154            let url = String::from_utf8_lossy(&url);
155            assert!(from_url(&url).is_err());
156        }
157    }
158
159    #[test]
160    fn normal_usage_ws() {
161        let addr = from_url("ws://127.0.0.1:8000").unwrap();
162        assert_eq!(addr, "/ip4/127.0.0.1/tcp/8000/ws".parse().unwrap());
163    }
164
165    #[test]
166    fn normal_usage_wss() {
167        let addr = from_url("wss://127.0.0.1:8000").unwrap();
168        assert_eq!(addr, "/ip4/127.0.0.1/tcp/8000/wss".parse().unwrap());
169    }
170
171    #[test]
172    fn default_ws_port() {
173        let addr = from_url("ws://127.0.0.1").unwrap();
174        assert_eq!(addr, "/ip4/127.0.0.1/tcp/80/ws".parse().unwrap());
175    }
176
177    #[test]
178    fn default_http_port() {
179        let addr = from_url("http://127.0.0.1").unwrap();
180        assert_eq!(addr, "/ip4/127.0.0.1/tcp/80/http".parse().unwrap());
181    }
182
183    #[test]
184    fn default_wss_port() {
185        let addr = from_url("wss://127.0.0.1").unwrap();
186        assert_eq!(addr, "/ip4/127.0.0.1/tcp/443/wss".parse().unwrap());
187    }
188
189    #[test]
190    fn default_https_port() {
191        let addr = from_url("https://127.0.0.1").unwrap();
192        assert_eq!(addr, "/ip4/127.0.0.1/tcp/443/https".parse().unwrap());
193    }
194
195    #[test]
196    fn dns_addr_ws() {
197        let addr = from_url("ws://example.com").unwrap();
198        assert_eq!(addr, "/dns/example.com/tcp/80/ws".parse().unwrap());
199    }
200
201    #[test]
202    fn dns_addr_http() {
203        let addr = from_url("http://example.com").unwrap();
204        assert_eq!(addr, "/dns/example.com/tcp/80/http".parse().unwrap());
205    }
206
207    #[test]
208    fn dns_addr_wss() {
209        let addr = from_url("wss://example.com").unwrap();
210        assert_eq!(addr, "/dns/example.com/tcp/443/wss".parse().unwrap());
211    }
212
213    #[test]
214    fn dns_addr_https() {
215        let addr = from_url("https://example.com").unwrap();
216        assert_eq!(addr, "/dns/example.com/tcp/443/https".parse().unwrap());
217    }
218
219    #[test]
220    fn bad_hostname() {
221        let addr = from_url("wss://127.0.0.1x").unwrap();
222        assert_eq!(addr, "/dns/127.0.0.1x/tcp/443/wss".parse().unwrap());
223    }
224
225    #[test]
226    fn wrong_scheme() {
227        match from_url("foo://127.0.0.1") {
228            Err(FromUrlErr::UnsupportedScheme) => {}
229            _ => panic!(),
230        }
231    }
232
233    #[test]
234    fn dns_and_port() {
235        let addr = from_url("http://example.com:1000").unwrap();
236        assert_eq!(addr, "/dns/example.com/tcp/1000/http".parse().unwrap());
237    }
238
239    #[test]
240    fn username_lossy() {
241        let addr = "http://foo@example.com:1000/";
242        assert!(from_url(addr).is_err());
243        assert!(from_url_lossy(addr).is_ok());
244        assert!(from_url("http://@example.com:1000/").is_ok());
245    }
246
247    #[test]
248    fn password_lossy() {
249        let addr = "http://:bar@example.com:1000/";
250        assert!(from_url(addr).is_err());
251        assert!(from_url_lossy(addr).is_ok());
252    }
253
254    #[test]
255    fn path_lossy() {
256        let addr = "http://example.com:1000/foo";
257        assert!(from_url(addr).is_err());
258        assert!(from_url_lossy(addr).is_ok());
259    }
260
261    #[test]
262    fn fragment_lossy() {
263        let addr = "http://example.com:1000/#foo";
264        assert!(from_url(addr).is_err());
265        assert!(from_url_lossy(addr).is_ok());
266    }
267
268    #[test]
269    fn unix() {
270        let addr = from_url("unix:/foo/bar").unwrap();
271        assert_eq!(addr, Multiaddr::from(Protocol::Unix("/foo/bar".into())));
272    }
273
274    #[test]
275    fn ws_path() {
276        let addr = from_url("ws://1.2.3.4:1000/foo/bar").unwrap();
277        assert_eq!(
278            addr,
279            "/ip4/1.2.3.4/tcp/1000/x-parity-ws/%2ffoo%2fbar"
280                .parse()
281                .unwrap()
282        );
283
284        let addr = from_url("ws://1.2.3.4:1000/").unwrap();
285        assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/ws".parse().unwrap());
286
287        let addr = from_url("wss://1.2.3.4:1000/foo/bar").unwrap();
288        assert_eq!(
289            addr,
290            "/ip4/1.2.3.4/tcp/1000/x-parity-wss/%2ffoo%2fbar"
291                .parse()
292                .unwrap()
293        );
294
295        let addr = from_url("wss://1.2.3.4:1000").unwrap();
296        assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/wss".parse().unwrap());
297    }
298}