ic-bn-lib 0.3.1

Internet Computer Boundary Nodes shared modules
pub mod cli;
#[cfg(feature = "clients-hyper")]
pub mod clients_hyper;
pub mod clients_reqwest;

use std::{fmt, net::SocketAddr, sync::Arc, time::Duration};

use async_trait::async_trait;
use http::HeaderValue;
use prometheus::{
    HistogramVec, IntCounterVec, IntGaugeVec, Registry, register_histogram_vec_with_registry,
    register_int_counter_vec_with_registry, register_int_gauge_vec_with_registry,
};
use strum::{Display, EnumString};

use crate::http::Error;

/// Generic HTTP client trait that is using Reqwest types
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait Client: Send + Sync + fmt::Debug {
    async fn execute(&self, req: reqwest::Request) -> Result<reqwest::Response, reqwest::Error>;
}

/// Generic HTTP client trait that is using `http` crate types
#[async_trait]
pub trait ClientHttp<B1, B2 = axum::body::Body>: Send + Sync + fmt::Debug {
    async fn execute(&self, req: http::Request<B1>) -> Result<http::Response<B2>, Error>;
}

/// HTTP versions to use
#[derive(Debug, Clone, Copy, Eq, PartialEq, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum HttpVersion {
    Http1,
    Http2,
    All,
}

/// HTTP client options
#[derive(Debug, Clone)]
pub struct ClientOptions {
    pub timeout_connect: Duration,
    pub timeout_read: Duration,
    pub timeout: Duration,
    pub pool_idle_timeout: Option<Duration>,
    pub pool_idle_max: Option<usize>,
    pub tcp_keepalive_delay: Option<Duration>,
    pub tcp_keepalive_interval: Option<Duration>,
    pub tcp_keepalive_retries: Option<u32>,
    pub http2_keepalive: Option<Duration>,
    pub http2_keepalive_timeout: Option<Duration>,
    pub http2_keepalive_idle: bool,
    pub happy_eyeballs_timeout: Duration,
    pub http_version: HttpVersion,
    pub user_agent: String,
    pub tls_config: Option<rustls::ClientConfig>,
    pub tls_fixed_name: Option<String>,
    pub dns_overrides: Vec<(String, SocketAddr)>,
}

impl Default for ClientOptions {
    fn default() -> Self {
        Self {
            timeout_connect: Duration::from_secs(10),
            timeout_read: Duration::from_secs(60),
            timeout: Duration::from_secs(120),
            pool_idle_timeout: None,
            pool_idle_max: None,
            tcp_keepalive_delay: None,
            tcp_keepalive_interval: None,
            tcp_keepalive_retries: None,
            http2_keepalive: None,
            http2_keepalive_timeout: None,
            http2_keepalive_idle: false,
            happy_eyeballs_timeout: Duration::from_millis(500),
            http_version: HttpVersion::All,
            user_agent: "Crab".into(),
            tls_config: None,
            tls_fixed_name: None,
            dns_overrides: vec![],
        }
    }
}

/// HTTP Client stats
#[derive(Debug, Clone)]
pub struct ClientStats {
    pub pool_size: usize,
    pub outstanding: usize,
}

/// Trait to get `ClientStats`
pub trait Stats {
    fn stats(&self) -> ClientStats;
}

/// `Client` that also emits `Stats`
pub trait ClientWithStats: Client + Stats {
    fn to_client(self: Arc<Self>) -> Arc<dyn Client>;
}

/// HTTP Client metrics
#[derive(Clone, Debug)]
struct Metrics {
    requests: IntCounterVec,
    requests_inflight: IntGaugeVec,
    request_duration: HistogramVec,
}

impl Metrics {
    pub fn new(registry: &Registry) -> Self {
        const LABELS: &[&str] = &["host"];

        Self {
            requests: register_int_counter_vec_with_registry!(
                format!("http_client_requests_total"),
                format!("Counts the number of requests"),
                LABELS,
                registry
            )
            .unwrap(),

            requests_inflight: register_int_gauge_vec_with_registry!(
                format!("http_client_requests_inflight"),
                format!("Counts the number of requests that are currently executed"),
                LABELS,
                registry
            )
            .unwrap(),

            request_duration: register_histogram_vec_with_registry!(
                format!("http_client_request_duration_sec"),
                format!("Records the duration of requests in seconds"),
                LABELS,
                [0.01, 0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2].to_vec(),
                registry
            )
            .unwrap(),
        }
    }
}

/// Create an HTTP header for Basic auth
pub fn basic_auth<U, P>(username: U, password: Option<P>) -> HeaderValue
where
    U: fmt::Display,
    P: fmt::Display,
{
    use base64::prelude::BASE64_STANDARD;
    use base64::write::EncoderWriter;
    use std::io::Write;

    let mut buf = b"Basic ".to_vec();
    {
        let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
        let _ = write!(encoder, "{username}:");
        if let Some(password) = password {
            let _ = write!(encoder, "{password}");
        }
    }

    let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
    header.set_sensitive(true);
    header
}