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