Skip to main content

volans_core/multiaddr/
from_url.rs

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