libp2p_webtransport_websys/
endpoint.rs

1use std::collections::HashSet;
2
3use js_sys::{Array, Uint8Array};
4use libp2p_identity::PeerId;
5use multiaddr::{Multiaddr, Protocol};
6use multihash::Multihash;
7
8use crate::{
9    bindings::{WebTransportHash, WebTransportOptions},
10    Error,
11};
12
13pub(crate) struct Endpoint {
14    pub(crate) host: String,
15    pub(crate) port: u16,
16    pub(crate) is_ipv6: bool,
17    pub(crate) certhashes: HashSet<Multihash<64>>,
18    pub(crate) remote_peer: Option<PeerId>,
19}
20
21impl Endpoint {
22    pub(crate) fn from_multiaddr(addr: &Multiaddr) -> Result<Self, Error> {
23        let mut host = None;
24        let mut port = None;
25        let mut found_quic = false;
26        let mut found_webtransport = false;
27        let mut certhashes = HashSet::new();
28        let mut remote_peer = None;
29        let mut is_ipv6 = false;
30
31        for proto in addr.iter() {
32            match proto {
33                Protocol::Ip4(addr) => {
34                    if host.is_some() {
35                        return Err(Error::InvalidMultiaddr("More than one host definitions"));
36                    }
37
38                    host = Some(addr.to_string());
39                }
40                Protocol::Ip6(addr) => {
41                    if host.is_some() {
42                        return Err(Error::InvalidMultiaddr("More than one host definitions"));
43                    }
44
45                    is_ipv6 = true;
46                    host = Some(addr.to_string());
47                }
48                Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain) => {
49                    if port.is_some() {
50                        return Err(Error::InvalidMultiaddr("More than one host definitions"));
51                    }
52
53                    host = Some(domain.to_string())
54                }
55                Protocol::Dnsaddr(_) => {
56                    return Err(Error::InvalidMultiaddr(
57                        "/dnsaddr not supported from within a browser",
58                    ));
59                }
60                Protocol::Udp(p) => {
61                    if port.is_some() {
62                        return Err(Error::InvalidMultiaddr("More than one port definitions"));
63                    }
64
65                    port = Some(p);
66                }
67                Protocol::Quic | Protocol::QuicV1 => {
68                    if host.is_none() || port.is_none() {
69                        return Err(Error::InvalidMultiaddr(
70                            "No host and port definition before /quic/webtransport",
71                        ));
72                    }
73
74                    found_quic = true;
75                }
76                Protocol::WebTransport => {
77                    if !found_quic {
78                        return Err(Error::InvalidMultiaddr(
79                            "/quic is not found before /webtransport",
80                        ));
81                    }
82
83                    found_webtransport = true;
84                }
85                Protocol::Certhash(hash) => {
86                    if !found_webtransport {
87                        return Err(Error::InvalidMultiaddr(
88                            "/certhashes must be after /quic/found_webtransport",
89                        ));
90                    }
91
92                    certhashes.insert(hash);
93                }
94                Protocol::P2p(peer) => {
95                    if remote_peer.is_some() {
96                        return Err(Error::InvalidMultiaddr("More than one peer definitions"));
97                    }
98
99                    remote_peer = Some(peer);
100                }
101                _ => {}
102            }
103        }
104
105        if !found_quic || !found_webtransport {
106            return Err(Error::InvalidMultiaddr(
107                "Not a /quic/webtransport multiaddr",
108            ));
109        }
110
111        let host = host.ok_or_else(|| Error::InvalidMultiaddr("Host is not defined"))?;
112        let port = port.ok_or_else(|| Error::InvalidMultiaddr("Port is not defined"))?;
113
114        Ok(Endpoint {
115            host,
116            port,
117            is_ipv6,
118            certhashes,
119            remote_peer,
120        })
121    }
122
123    pub(crate) fn url(&self) -> String {
124        let host = &self.host;
125        let port = self.port;
126
127        if self.is_ipv6 {
128            format!("https://[{host}]:{port}/.well-known/libp2p-webtransport?type=noise")
129        } else {
130            format!("https://{host}:{port}/.well-known/libp2p-webtransport?type=noise")
131        }
132    }
133
134    pub(crate) fn webtransport_opts(&self) -> WebTransportOptions {
135        let mut opts = WebTransportOptions::new();
136        let hashes = Array::new();
137
138        for hash in &self.certhashes {
139            let digest = Uint8Array::from(hash.digest());
140
141            let mut jshash = WebTransportHash::new();
142            jshash.algorithm("sha-256").value(&digest);
143
144            hashes.push(&jshash);
145        }
146
147        opts.server_certificate_hashes(&hashes);
148
149        opts
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use std::str::FromStr;
156
157    use super::*;
158
159    fn multihash_from_str(s: &str) -> Multihash<64> {
160        let (_base, bytes) = multibase::decode(s).unwrap();
161        Multihash::from_bytes(&bytes).unwrap()
162    }
163
164    #[test]
165    fn valid_webtransport_multiaddr() {
166        let addr = Multiaddr::from_str("/ip4/127.0.0.1/udp/44874/quic-v1/webtransport/certhash/uEiCaDd1Ca1A8IVJ3hsIxIyi11cwxaDKqzVrBkGJbKZU5ng/certhash/uEiDv-VGW8oXxui_G_Kqp-87YjvET-Hr2qYAMYPePJDcsjQ/p2p/12D3KooWR7EfNv5SLtgjMRjUwR8AvNu3hP4fLrtSa9fmHHXKYWNG").unwrap();
167        let endpoint = Endpoint::from_multiaddr(&addr).unwrap();
168
169        assert_eq!(endpoint.host, "127.0.0.1");
170        assert_eq!(endpoint.port, 44874);
171        assert_eq!(endpoint.certhashes.len(), 2);
172
173        assert!(endpoint.certhashes.contains(&multihash_from_str(
174            "uEiCaDd1Ca1A8IVJ3hsIxIyi11cwxaDKqzVrBkGJbKZU5ng"
175        )));
176
177        assert!(endpoint.certhashes.contains(&multihash_from_str(
178            "uEiDv-VGW8oXxui_G_Kqp-87YjvET-Hr2qYAMYPePJDcsjQ"
179        )));
180
181        assert_eq!(
182            endpoint.remote_peer.unwrap(),
183            PeerId::from_str("12D3KooWR7EfNv5SLtgjMRjUwR8AvNu3hP4fLrtSa9fmHHXKYWNG").unwrap()
184        );
185
186        assert_eq!(
187            endpoint.url(),
188            "https://127.0.0.1:44874/.well-known/libp2p-webtransport?type=noise"
189        );
190    }
191
192    #[test]
193    fn valid_webtransport_multiaddr_without_certhashes() {
194        let addr = Multiaddr::from_str("/ip4/127.0.0.1/udp/44874/quic-v1/webtransport/p2p/12D3KooWR7EfNv5SLtgjMRjUwR8AvNu3hP4fLrtSa9fmHHXKYWNG").unwrap();
195        let endpoint = Endpoint::from_multiaddr(&addr).unwrap();
196
197        assert_eq!(endpoint.host, "127.0.0.1");
198        assert_eq!(endpoint.port, 44874);
199        assert_eq!(endpoint.certhashes.len(), 0);
200        assert_eq!(
201            endpoint.remote_peer.unwrap(),
202            PeerId::from_str("12D3KooWR7EfNv5SLtgjMRjUwR8AvNu3hP4fLrtSa9fmHHXKYWNG").unwrap()
203        );
204    }
205
206    #[test]
207    fn ipv6_webtransport() {
208        let addr = Multiaddr::from_str("/ip6/::1/udp/44874/quic-v1/webtransport/certhash/uEiCaDd1Ca1A8IVJ3hsIxIyi11cwxaDKqzVrBkGJbKZU5ng/certhash/uEiDv-VGW8oXxui_G_Kqp-87YjvET-Hr2qYAMYPePJDcsjQ/p2p/12D3KooWR7EfNv5SLtgjMRjUwR8AvNu3hP4fLrtSa9fmHHXKYWNG").unwrap();
209        let endpoint = Endpoint::from_multiaddr(&addr).unwrap();
210
211        assert_eq!(endpoint.host, "::1");
212        assert_eq!(endpoint.port, 44874);
213        assert_eq!(
214            endpoint.url(),
215            "https://[::1]:44874/.well-known/libp2p-webtransport?type=noise"
216        );
217    }
218
219    #[test]
220    fn dns_webtransport() {
221        let addr = Multiaddr::from_str("/dns/libp2p.io/udp/44874/quic-v1/webtransport/certhash/uEiCaDd1Ca1A8IVJ3hsIxIyi11cwxaDKqzVrBkGJbKZU5ng/certhash/uEiDv-VGW8oXxui_G_Kqp-87YjvET-Hr2qYAMYPePJDcsjQ/p2p/12D3KooWR7EfNv5SLtgjMRjUwR8AvNu3hP4fLrtSa9fmHHXKYWNG").unwrap();
222        let endpoint = Endpoint::from_multiaddr(&addr).unwrap();
223
224        assert_eq!(endpoint.host, "libp2p.io");
225        assert_eq!(endpoint.port, 44874);
226        assert_eq!(
227            endpoint.url(),
228            "https://libp2p.io:44874/.well-known/libp2p-webtransport?type=noise"
229        );
230    }
231}