Skip to main content

ic_bn_lib/
lib.rs

1// Needed for certain macros
2#![recursion_limit = "256"]
3#![warn(clippy::nursery)]
4#![warn(tail_expr_drop_order)]
5#![allow(clippy::cognitive_complexity)]
6#![allow(clippy::field_reassign_with_default)]
7#![allow(clippy::collapsible_if)]
8
9#[cfg(feature = "custom-domains")]
10pub mod custom_domains;
11pub mod http;
12pub mod network;
13pub mod pubsub;
14#[cfg(feature = "smtp")]
15pub mod smtp;
16pub mod tasks;
17pub mod tests;
18pub mod tls;
19pub mod utils;
20#[cfg(feature = "vector")]
21pub mod vector;
22
23use std::{fs::File, net::IpAddr, path::Path};
24
25use anyhow::{Context, anyhow};
26use bytes::Bytes;
27use futures::StreamExt;
28use ic_bn_lib_common::Error;
29use serde::Serialize;
30use tokio::io::AsyncWriteExt;
31
32pub use hickory_proto;
33pub use hickory_resolver;
34pub use hyper;
35pub use hyper_util;
36pub use ic_agent;
37pub use ic_bn_lib_common;
38#[cfg(feature = "smtp")]
39pub use mail_auth;
40pub use prometheus;
41#[cfg(feature = "acme")]
42pub use rcgen;
43pub use reqwest;
44pub use rustls;
45#[cfg(feature = "acme-alpn")]
46pub use rustls_acme;
47pub use uuid;
48
49/// Converts a string representation to an `EmailAddress`. Panics when an error occurs.
50#[macro_export]
51macro_rules! email {
52    ($email:expr) => {{ $crate::smtp::address::EmailAddress::from_text($email).unwrap() }};
53}
54
55/// Error to be used with `retry_async` macro
56/// which indicates whether it should be retried or not.
57#[derive(thiserror::Error, Debug)]
58pub enum RetryError {
59    #[error("Permanent error: {0:?}")]
60    Permanent(anyhow::Error),
61    #[error("Transient error: {0:?}")]
62    Transient(anyhow::Error),
63}
64
65/// Downloads the given url to given path.
66/// Destination folder must exist.
67pub fn download_url_to(url: &str, path: &Path) -> Result<u64, Error> {
68    let mut r = reqwest::blocking::get(url).context("unable to perform HTTP request")?;
69    if !r.status().is_success() {
70        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
71    }
72
73    let mut file = File::create(path).context("could not create file")?;
74    Ok(r.copy_to(&mut file)
75        .context("unable to write body to file")?)
76}
77
78/// Downloads the given url and returns it as Bytes
79pub fn download_url(url: &str) -> Result<Bytes, Error> {
80    let r = reqwest::blocking::get(url).context("unable to perform HTTP request")?;
81    if !r.status().is_success() {
82        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
83    }
84
85    Ok(r.bytes().context("unable to fetch file")?)
86}
87
88/// Downloads the given url to given path.
89/// Destination folder must exist.
90pub async fn download_url_to_async(url: &str, path: &Path) -> Result<(), Error> {
91    let r = reqwest::get(url)
92        .await
93        .context("unable to perform HTTP request")?;
94    if !r.status().is_success() {
95        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
96    }
97
98    let mut file = tokio::fs::File::create(path)
99        .await
100        .context("could not create file")?;
101
102    let mut stream = r.bytes_stream();
103    while let Some(v) = stream.next().await {
104        file.write(&v.context("unable to read chunk")?)
105            .await
106            .context("unable to write chunk")?;
107    }
108
109    Ok(())
110}
111
112/// Downloads the given url and returns it as Bytes
113pub async fn download_url_async(url: &str) -> Result<Bytes, Error> {
114    let r = reqwest::get(url)
115        .await
116        .context("unable to perform HTTP request")?;
117
118    if !r.status().is_success() {
119        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
120    }
121
122    Ok(r.bytes().await.context("unable to fetch file")?)
123}
124
125/// Retrying async closures/functions holding mutable references is a pain in Rust.
126/// So, for now, we'll have to use a macro to work that around.
127#[macro_export]
128macro_rules! retry_async {
129    ($f:expr, $timeout:expr, $delay:expr) => {{
130        use rand::{Rng, SeedableRng};
131        // SmallRng is Send which we require
132        let mut rng = rand::rngs::SmallRng::from_entropy();
133
134        let start = std::time::Instant::now();
135        let mut delay = $delay;
136
137        let result = loop {
138            // Run the function wrapping it into Tokio timeout future so
139            // its execution time doesn't exceed our configured limit
140            let Ok(res) = tokio::time::timeout($timeout, $f).await else {
141                break Err(anyhow::anyhow!("Timed out"));
142            };
143
144            let err = match res {
145                Ok(v) => break Ok(v),
146                Err($crate::RetryError::Permanent(e)) => break Err(e),
147                Err($crate::RetryError::Transient(e)) => e,
148            };
149
150            let left = $timeout.saturating_sub(start.elapsed());
151            if left == std::time::Duration::ZERO {
152                break Err(err);
153            }
154
155            delay = left.min(delay * 2);
156            // Generate a random jitter in 0.0..0.1 range
157            let jitter: f64 = (rng.r#gen::<f64>() / 10.0);
158            let d64 = delay.as_secs_f64();
159            delay = Duration::from_secs_f64(d64.mul_add(0.95, d64 * jitter));
160            tokio::time::sleep(delay).await;
161        };
162
163        result
164    }};
165
166    ($f:expr, $timeout:expr) => {
167        retry_async!($f, $timeout, Duration::from_millis(500))
168    };
169
170    ($f:expr) => {
171        retry_async!($f, Duration::from_secs(60), Duration::from_millis(500))
172    };
173}
174
175/// Returns family of an IP address
176pub trait IpFamily {
177    fn family(&self) -> &'static str;
178}
179
180impl IpFamily for IpAddr {
181    fn family(&self) -> &'static str {
182        if self.is_ipv4() { "v4" } else { "v6" }
183    }
184}
185
186/// Converts bool to yes/no static str
187pub trait BoolYesNo {
188    fn yesno(&self) -> &'static str;
189}
190
191impl BoolYesNo for bool {
192    fn yesno(&self) -> &'static str {
193        if *self { "yes" } else { "no" }
194    }
195}
196
197pub trait SerializeOption<T> {
198    /// Serializes `Option<T>` as either inner value or provided default
199    fn serialize_or<'t, O>(&'t self, otherwise: O) -> SerializeOr<'t, T, O>;
200}
201
202pub struct SerializeOr<'t, T, O> {
203    option: &'t Option<T>,
204    otherwise: O,
205}
206
207impl<'t, T: Serialize, O: Serialize> Serialize for SerializeOr<'t, T, O> {
208    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
209    where
210        S: serde::Serializer,
211    {
212        match self.option {
213            Some(v) => v.serialize(serializer),
214            None => self.otherwise.serialize(serializer),
215        }
216    }
217}
218
219impl<T> SerializeOption<T> for Option<T> {
220    fn serialize_or<'t, O>(&'t self, otherwise: O) -> SerializeOr<'t, T, O> {
221        SerializeOr {
222            option: self,
223            otherwise,
224        }
225    }
226}
227
228#[macro_export]
229macro_rules! dyn_event {
230    ($lvl:ident, $($arg:tt)+) => {
231        match $lvl {
232            ::tracing::Level::TRACE => ::tracing::trace!($($arg)+),
233            ::tracing::Level::DEBUG => ::tracing::debug!($($arg)+),
234            ::tracing::Level::INFO => ::tracing::info!($($arg)+),
235            ::tracing::Level::WARN => ::tracing::warn!($($arg)+),
236            ::tracing::Level::ERROR => ::tracing::error!($($arg)+),
237        }
238    };
239}
240
241/// Truncates the given string to around n *bytes*,
242/// on the closest UTF-8 code point boundary.
243pub fn truncate(s: &str, n: usize) -> &str {
244    let n = s.len().min(n);
245    let m = (0..=n)
246        .rfind(|m| s.is_char_boundary(*m))
247        .unwrap_or_default();
248    &s[..m]
249}
250
251#[cfg(test)]
252mod test {
253    use super::*;
254
255    #[test]
256    fn test_truncate() {
257        assert_eq!(truncate("foobarbaz", 4), "foob");
258        assert_eq!(truncate("tättähäärä härkä", 12), "tättähää");
259        assert_eq!(truncate("", 99), "");
260        assert_eq!(truncate("🏁", 2), "");
261        assert_eq!(truncate("foobarbaz", 99), "foobarbaz");
262    }
263}