ng_net/
utils.rs

1/*
2 * Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
3 * All rights reserved.
4 * Licensed under the Apache License, Version 2.0
5 * <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
6 * or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
7 * at your option. All files in the project carrying such
8 * notice may not be copied, modified, or distributed except
9 * according to those terms.
10*/
11
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
13
14use async_std::task;
15use ed25519_dalek::*;
16use futures::{channel::mpsc, Future};
17use lazy_static::lazy_static;
18use noise_protocol::U8Array;
19use noise_protocol::DH;
20use noise_rust_crypto::sensitive::Sensitive;
21use regex::Regex;
22use url::Host;
23use url::Url;
24
25#[allow(unused_imports)]
26use ng_repo::errors::*;
27use ng_repo::types::PubKey;
28use ng_repo::{log::*, types::PrivKey};
29
30use crate::types::*;
31use crate::NG_BOOTSTRAP_LOCAL_PATH;
32use crate::WS_PORT;
33
34#[doc(hidden)]
35#[cfg(target_arch = "wasm32")]
36pub fn spawn_and_log_error<F>(fut: F) -> task::JoinHandle<()>
37where
38    F: Future<Output = ResultSend<()>> + 'static,
39{
40    task::spawn_local(async move {
41        if let Err(e) = fut.await {
42            log_err!("EXCEPTION {}", e)
43        }
44    })
45}
46#[cfg(target_arch = "wasm32")]
47pub type ResultSend<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
48
49#[cfg(not(target_arch = "wasm32"))]
50pub type ResultSend<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
51
52#[doc(hidden)]
53#[cfg(not(target_arch = "wasm32"))]
54pub fn spawn_and_log_error<F>(fut: F) -> task::JoinHandle<()>
55where
56    F: Future<Output = ResultSend<()>> + Send + 'static,
57{
58    task::spawn(async move {
59        if let Err(e) = fut.await {
60            log_err!("{}", e)
61        }
62    })
63}
64
65#[cfg(target_arch = "wasm32")]
66#[cfg(debug_assertions)]
67const APP_PREFIX: &str = "http://localhost:14400";
68
69#[cfg(target_arch = "wasm32")]
70#[cfg(not(debug_assertions))]
71const APP_PREFIX: &str = "";
72
73pub fn decode_invitation_string(string: String) -> Option<Invitation> {
74    Invitation::try_from(string).ok()
75}
76
77lazy_static! {
78    #[doc(hidden)]
79    static ref RE_IPV6_WITH_PORT: Regex =
80        Regex::new(r"^\[([0-9a-fA-F:]{3,39})\](\:\d{1,5})?$").unwrap();
81}
82
83#[doc(hidden)]
84pub fn parse_ipv4_and_port_for(
85    string: String,
86    for_option: &str,
87    default_port: u16,
88) -> Result<(Ipv4Addr, u16), NgError> {
89    let parts: Vec<&str> = string.split(":").collect();
90    let ipv4 = parts[0].parse::<Ipv4Addr>().map_err(|_| {
91        NgError::ConfigError(format!(
92            "The <IPv4:PORT> value submitted for the {} option is invalid.",
93            for_option
94        ))
95    })?;
96
97    let port = if parts.len() > 1 {
98        match serde_json::from_str::<u16>(parts[1]) {
99            Err(_) => default_port,
100            Ok(p) => {
101                if p == 0 {
102                    default_port
103                } else {
104                    p
105                }
106            }
107        }
108    } else {
109        default_port
110    };
111    Ok((ipv4, port))
112}
113
114#[doc(hidden)]
115pub fn parse_ip_and_port_for(string: String, for_option: &str) -> Result<BindAddress, NgError> {
116    let bind = parse_ip_and_port_for_(string, for_option)?;
117    Ok(BindAddress {
118        ip: (&bind.0).into(),
119        port: bind.1,
120    })
121}
122
123fn parse_ip_and_port_for_(string: String, for_option: &str) -> Result<(IpAddr, u16), NgError> {
124    let c = RE_IPV6_WITH_PORT.captures(&string);
125    let ipv6;
126    let port;
127    if c.is_some() && c.as_ref().unwrap().get(1).is_some() {
128        let cap = c.unwrap();
129        let ipv6_str = cap.get(1).unwrap().as_str();
130        port = match cap.get(2) {
131            None => WS_PORT,
132            Some(p) => {
133                let mut chars = p.as_str().chars();
134                chars.next();
135                match serde_json::from_str::<u16>(chars.as_str()) {
136                    Err(_) => WS_PORT,
137                    Ok(p) => {
138                        if p == 0 {
139                            WS_PORT
140                        } else {
141                            p
142                        }
143                    }
144                }
145            }
146        };
147        let ipv6 = ipv6_str.parse::<Ipv6Addr>().map_err(|_| {
148            NgError::ConfigError(format!(
149                "The <[IPv6]:PORT> value submitted for the {} option is invalid.",
150                for_option
151            ))
152        })?;
153        Ok((IpAddr::V6(ipv6), port))
154    } else {
155        // we try just an IPV6 without port
156        let ipv6_res = string.parse::<Ipv6Addr>();
157        if ipv6_res.is_err() {
158            // let's try IPv4
159
160            parse_ipv4_and_port_for(string, for_option, WS_PORT)
161                .map(|ipv4| (IpAddr::V4(ipv4.0), ipv4.1))
162        } else {
163            ipv6 = ipv6_res.unwrap();
164            port = WS_PORT;
165            Ok((IpAddr::V6(ipv6), port))
166        }
167    }
168}
169
170pub fn check_is_local_url(bootstrap: &BrokerServerV0, location: &String) -> Option<String> {
171    if location.starts_with(NG_APP_URL) {
172        match &bootstrap.server_type {
173            BrokerServerTypeV0::Public(_) | BrokerServerTypeV0::BoxPublicDyn(_) => {
174                return Some(APP_NG_WS_URL.to_string());
175            }
176            _ => {}
177        }
178    } else if let BrokerServerTypeV0::Domain(domain) = &bootstrap.server_type {
179        let url = format!("https://{}", domain);
180        if location.starts_with(&url) {
181            return Some(url);
182        }
183    } else {
184        // localhost
185        if location.starts_with(LOCAL_URLS[0])
186            || location.starts_with(LOCAL_URLS[1])
187            || location.starts_with(LOCAL_URLS[2])
188        {
189            if let BrokerServerTypeV0::Localhost(port) = bootstrap.server_type {
190                return Some(local_http_url(&port));
191            }
192        }
193        // a private address
194        else if location.starts_with("http://") {
195            let url = Url::parse(location).unwrap();
196            match url.host() {
197                Some(Host::Ipv4(ip)) => {
198                    if is_ipv4_private(&ip) {
199                        let res = bootstrap.first_ipv4_http();
200                        if res.is_some() {
201                            return res;
202                        }
203                    }
204                }
205                Some(Host::Ipv6(ip)) => {
206                    if is_ipv6_private(&ip) {
207                        let res = bootstrap.first_ipv6_http();
208                        if res.is_some() {
209                            return res;
210                        }
211                    }
212                }
213                _ => {}
214            }
215        }
216    }
217    None
218}
219
220#[cfg(target_arch = "wasm32")]
221async fn retrieve_ng_bootstrap(location: &String) -> Option<LocalBootstrapInfo> {
222    let prefix = if APP_PREFIX == "" {
223        let url = Url::parse(location).unwrap();
224        url.origin().unicode_serialization()
225    } else {
226        APP_PREFIX.to_string()
227    };
228    let url = format!("{}{}", prefix, NG_BOOTSTRAP_LOCAL_PATH);
229    log_info!("url {}", url);
230    let resp = reqwest::get(url).await;
231    //log_info!("{:?}", resp);
232    if resp.is_ok() {
233        let resp = resp.unwrap().json::<LocalBootstrapInfo>().await;
234        return if resp.is_ok() {
235            Some(resp.unwrap())
236        } else {
237            None
238        };
239    } else {
240        //log_info!("err {}", resp.unwrap_err());
241        return None;
242    }
243}
244
245#[cfg(not(target_arch = "wasm32"))]
246pub async fn retrieve_ng_bootstrap(location: &String) -> Option<LocalBootstrapInfo> {
247    let url = Url::parse(location).unwrap();
248    let prefix = url.origin().unicode_serialization();
249    let url = format!("{}{}", prefix, NG_BOOTSTRAP_LOCAL_PATH);
250    log_info!("url {}", url);
251    let resp = reqwest::get(url).await;
252    //log_info!("{:?}", resp);
253    if resp.is_ok() {
254        let resp = resp.unwrap().json::<LocalBootstrapInfo>().await;
255        return if resp.is_ok() {
256            Some(resp.unwrap())
257        } else {
258            None
259        };
260    } else {
261        //log_info!("err {}", resp.unwrap_err());
262        return None;
263    }
264}
265
266// #[cfg(target_arch = "wasm32")]
267// pub async fn retrieve_domain(location: String) -> Option<String> {
268//     let info = retrieve_ng_bootstrap(&location).await;
269//     if info.is_none() {
270//         return None;
271//     }
272//     for bootstrap in info.unwrap().servers() {
273//         let res = bootstrap.get_domain();
274//         if res.is_some() {
275//             return res;
276//         }
277//     }
278//     None
279// }
280
281#[cfg(target_arch = "wasm32")]
282pub async fn retrieve_local_url(location: String) -> Option<String> {
283    let info = retrieve_ng_bootstrap(&location).await;
284    if info.is_none() {
285        return None;
286    }
287    for bootstrap in info.unwrap().servers() {
288        let res = check_is_local_url(bootstrap, &location);
289        if res.is_some() {
290            return res;
291        }
292    }
293    None
294}
295
296#[cfg(target_arch = "wasm32")]
297pub async fn retrieve_local_bootstrap(
298    location_string: String,
299    invite_string: Option<String>,
300    must_be_public: bool,
301) -> Option<Invitation> {
302    let invite1: Option<Invitation> = if invite_string.is_some() {
303        let invitation: Result<Invitation, NgError> = invite_string.clone().unwrap().try_into();
304        invitation.ok()
305    } else {
306        None
307    };
308    log_debug!("{}", location_string);
309    log_debug!("invite_string {:?} invite1{:?}", invite_string, invite1);
310
311    let invite2: Option<Invitation> = {
312        let info = retrieve_ng_bootstrap(&location_string).await;
313        if info.is_none() {
314            None
315        } else {
316            let inv: Invitation = info.unwrap().into();
317            Some(inv)
318        }
319    };
320
321    let res = if invite1.is_none() {
322        invite2
323    } else if invite2.is_none() {
324        invite1
325    } else {
326        invite1.map(|i| i.intersects(invite2.unwrap()))
327    };
328
329    if res.is_some() {
330        for server in res.as_ref().unwrap().get_servers() {
331            if must_be_public && server.is_public_server()
332                || !must_be_public && check_is_local_url(server, &location_string).is_some()
333            {
334                return res;
335            }
336        }
337        return None;
338    }
339    res
340}
341
342pub fn sensitive_from_privkey(privkey: PrivKey) -> Sensitive<[u8; 32]> {
343    // we copy the key here, because otherwise the 2 zeroize would conflict. as the drop of the PrivKey might be called before the one of Sensitive
344    let mut bits: [u8; 32] = [0u8; 32];
345    bits.copy_from_slice(privkey.slice());
346    Sensitive::<[u8; 32]>::from_slice(&bits)
347}
348
349pub fn dh_privkey_from_sensitive(privkey: Sensitive<[u8; 32]>) -> PrivKey {
350    // we copy the key here, because otherwise the 2 zeroize would conflict. as the drop of the Sensitive might be called before the one of PrivKey
351    let mut bits: [u8; 32] = [0u8; 32];
352    bits.copy_from_slice(privkey.as_slice());
353    PrivKey::X25519PrivKey(bits)
354}
355
356pub type Sender<T> = mpsc::UnboundedSender<T>;
357pub type Receiver<T> = mpsc::UnboundedReceiver<T>;
358
359pub fn gen_dh_keys() -> (PrivKey, PubKey) {
360    let pri = noise_rust_crypto::X25519::genkey();
361    let publ = noise_rust_crypto::X25519::pubkey(&pri);
362
363    (dh_privkey_from_sensitive(pri), PubKey::X25519PubKey(publ))
364}
365
366pub struct Dual25519Keys {
367    pub x25519_priv: Sensitive<[u8; 32]>,
368    pub x25519_public: [u8; 32],
369    pub ed25519_priv: SecretKey,
370    pub ed25519_pub: PublicKey,
371}
372
373impl Dual25519Keys {
374    pub fn generate() -> Self {
375        let mut random = Sensitive::<[u8; 32]>::new();
376        getrandom::getrandom(&mut *random).expect("getrandom failed");
377
378        let ed25519_priv = SecretKey::from_bytes(&random.as_slice()).unwrap();
379        let exp: ExpandedSecretKey = (&ed25519_priv).into();
380        let mut exp_bytes = exp.to_bytes();
381        let ed25519_pub: PublicKey = (&ed25519_priv).into();
382        for byte in &mut exp_bytes[32..] {
383            *byte = 0;
384        }
385        let mut bits = Sensitive::<[u8; 32]>::from_slice(&exp_bytes[0..32]);
386        bits[0] &= 248;
387        bits[31] &= 127;
388        bits[31] |= 64;
389
390        let x25519_public = noise_rust_crypto::X25519::pubkey(&bits);
391
392        Self {
393            x25519_priv: bits,
394            x25519_public,
395            ed25519_priv,
396            ed25519_pub,
397        }
398    }
399}
400
401pub fn get_domain_without_port(domain: &String) -> String {
402    let parts: Vec<&str> = domain.split(':').collect();
403    parts[0].to_string()
404}
405
406pub fn get_domain_without_port_443(domain: &str) -> &str {
407    let parts: Vec<&str> = domain.split(':').collect();
408    if parts.len() > 1 && parts[1] == "443" {
409        return parts[0];
410    }
411    domain
412}
413
414pub fn is_public_ipv4(ip: &Ipv4Addr) -> bool {
415    // TODO, use core::net::Ipv4Addr.is_global when it will be stable
416    return is_ipv4_global(ip);
417}
418
419pub fn is_public_ipv6(ip: &Ipv6Addr) -> bool {
420    // TODO, use core::net::Ipv6Addr.is_global when it will be stable
421    return is_ipv6_global(ip);
422}
423
424pub fn is_public_ip(ip: &IpAddr) -> bool {
425    match ip {
426        IpAddr::V4(v4) => is_public_ipv4(v4),
427        IpAddr::V6(v6) => is_public_ipv6(v6),
428    }
429}
430
431pub fn is_private_ip(ip: &IpAddr) -> bool {
432    match ip {
433        IpAddr::V4(v4) => is_ipv4_private(v4),
434        IpAddr::V6(v6) => is_ipv6_private(v6),
435    }
436}
437
438#[must_use]
439#[inline]
440pub const fn is_ipv4_shared(addr: &Ipv4Addr) -> bool {
441    addr.octets()[0] == 100 && (addr.octets()[1] & 0b1100_0000 == 0b0100_0000)
442}
443
444#[must_use]
445#[inline]
446pub const fn is_ipv4_benchmarking(addr: &Ipv4Addr) -> bool {
447    addr.octets()[0] == 198 && (addr.octets()[1] & 0xfe) == 18
448}
449
450#[must_use]
451#[inline]
452pub const fn is_ipv4_reserved(addr: &Ipv4Addr) -> bool {
453    addr.octets()[0] & 240 == 240 && !addr.is_broadcast()
454}
455
456#[must_use]
457#[inline]
458pub const fn is_ipv4_private(addr: &Ipv4Addr) -> bool {
459    addr.is_private() || addr.is_link_local()
460}
461
462#[must_use]
463#[inline]
464pub const fn is_ipv4_global(addr: &Ipv4Addr) -> bool {
465    !(addr.octets()[0] == 0 // "This network"
466            || addr.is_private()
467            || is_ipv4_shared(addr)
468            || addr.is_loopback()
469            || addr.is_link_local()
470            // addresses reserved for future protocols (`192.0.0.0/24`)
471            ||(addr.octets()[0] == 192 && addr.octets()[1] == 0 && addr.octets()[2] == 0)
472            || addr.is_documentation()
473            || is_ipv4_benchmarking(addr)
474            || is_ipv4_reserved(addr)
475            || addr.is_broadcast())
476}
477
478#[must_use]
479#[inline]
480pub const fn is_ipv6_unique_local(addr: &Ipv6Addr) -> bool {
481    (addr.segments()[0] & 0xfe00) == 0xfc00
482}
483
484#[must_use]
485#[inline]
486pub const fn is_ipv6_unicast_link_local(addr: &Ipv6Addr) -> bool {
487    (addr.segments()[0] & 0xffc0) == 0xfe80
488}
489
490#[must_use]
491#[inline]
492pub const fn is_ipv6_documentation(addr: &Ipv6Addr) -> bool {
493    (addr.segments()[0] == 0x2001) && (addr.segments()[1] == 0xdb8)
494}
495
496#[must_use]
497#[inline]
498pub const fn is_ipv6_private(addr: &Ipv6Addr) -> bool {
499    is_ipv6_unique_local(addr)
500}
501
502#[must_use]
503#[inline]
504pub const fn is_ipv6_global(addr: &Ipv6Addr) -> bool {
505    !(addr.is_unspecified()
506        || addr.is_loopback()
507        // IPv4-mapped Address (`::ffff:0:0/96`)
508        || matches!(addr.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
509        // IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
510        || matches!(addr.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
511        // Discard-Only Address Block (`100::/64`)
512        || matches!(addr.segments(), [0x100, 0, 0, 0, _, _, _, _])
513        // IETF Protocol Assignments (`2001::/23`)
514        || (matches!(addr.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
515            && !(
516                // Port Control Protocol Anycast (`2001:1::1`)
517                u128::from_be_bytes(addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
518                // Traversal Using Relays around NAT Anycast (`2001:1::2`)
519                || u128::from_be_bytes(addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
520                // AMT (`2001:3::/32`)
521                || matches!(addr.segments(), [0x2001, 3, _, _, _, _, _, _])
522                // AS112-v6 (`2001:4:112::/48`)
523                || matches!(addr.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
524                // ORCHIDv2 (`2001:20::/28`)
525                || matches!(addr.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x2F)
526            ))
527        || is_ipv6_documentation(addr)
528        || is_ipv6_unique_local(addr)
529        || is_ipv6_unicast_link_local(addr))
530}