pass-it-on 0.17.3

A library that provides a simple notification client and server that receives messages and passes them on to endpoints
Documentation
//! HTTP [`Interface`] and [`InterfaceConfig`] implementation
//!
//! # Server Configuration Example
//! ## Configuration for Localhost
//! ```toml
//! [[server.interface]]
//! type = "http"
//! host = "http://localhost"
//! port = 8080
//! ```
//!
//! ## Configuration with TLS
//! ```toml
//! [[server.interface]]
//! type = "http"
//! host = "example.com"
//! port = 8080
//! tls = true
//! tls_cert_path = "/path/to/certificate/cert.pem"
//! tls_key_path = "/path/to/private/key/key.pem"
//! ```
//!
//! # Client Configuration Example
//! ```toml
//! [[client.interface]]
//! type = "http"
//! host = "127.0.0.1"
//! port = 8080
//! ```

#[cfg(feature = "http-client")]
pub(crate) mod http_client;
#[cfg(feature = "http-server")]
pub(crate) mod http_server;

use crate::interfaces::{Interface, InterfaceConfig};
use crate::notifications::Notification;
use crate::{Error, CRATE_VERSION};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::PathBuf;
use tokio::sync::{broadcast, mpsc, watch};
use url::{ParseError, Url};

const DEFAULT_HOST: &str = "http://0.0.0.0";
const HTTP: &str = "http";
const HTTPS: &str = "https";
const DEFAULT_PORT: u16 = 8080;
const BASE_PATH: &str = "pass-it-on";
const NOTIFICATION_PATH: &str = "notification";
const VERSION_PATH: &str = "version";

#[derive(Debug, Deserialize, Serialize)]
struct Version {
    version: String,
}

/// Data structure to represent the HTTP Socket [`Interface`].
#[derive(Debug, Clone)]
pub struct HttpSocketInterface {
    host: Url,
    tls: bool,
    port: u16,
    tls_cert_path: Option<PathBuf>,
    tls_key_path: Option<PathBuf>,
}

/// Data structure to represent the HTTP Socket [`InterfaceConfig`].
#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone)]
#[serde(default)]
pub(crate) struct HttpSocketConfigFile {
    pub host: String,
    pub tls: Option<bool>,
    pub port: i64,
    pub tls_cert_path: Option<String>,
    pub tls_key_path: Option<String>,
}

impl Version {
    fn new() -> Self {
        Self { version: CRATE_VERSION.to_string() }
    }
}

impl HttpSocketInterface {
    /// Create a new `HttpSocketInterface`.
    pub fn new<P: AsRef<str>>(host_url: &Url, cert_path: Option<P>, key_path: Option<P>) -> Self {
        let host = host_url.clone();
        let tls = host.scheme().eq_ignore_ascii_case(HTTPS);
        let port = host.port().unwrap_or(DEFAULT_PORT);
        let tls_cert_path = cert_path.map(|p| PathBuf::from(p.as_ref()));
        let tls_key_path = key_path.map(|p| PathBuf::from(p.as_ref()));
        Self { host, tls, port, tls_cert_path, tls_key_path }
    }

    /// Return the IP address.
    pub fn host(&self) -> &str {
        self.host.as_str()
    }

    /// Return the IP address if it exists or the default address(127.0.0.1).
    pub fn sockets(&self) -> Result<Vec<SocketAddr>, Error> {
        Ok(self.host.socket_addrs(|| Some(self.port()))?)
    }

    /// Return the port.
    pub fn port(&self) -> u16 {
        self.port
    }

    /// Return if interface should use TLS
    pub fn tls(&self) -> bool {
        self.tls
    }

    /// Return path the TLS certificate
    pub fn tls_cert_path(&self) -> &Option<PathBuf> {
        &self.tls_cert_path
    }

    /// Return path the TLS private key
    pub fn tls_key_path(&self) -> &Option<PathBuf> {
        &self.tls_key_path
    }
}

impl Default for HttpSocketConfigFile {
    fn default() -> Self {
        Self {
            host: DEFAULT_HOST.into(),
            tls: None,
            port: DEFAULT_PORT as i64,
            tls_cert_path: None,
            tls_key_path: None,
        }
    }
}

impl Default for HttpSocketInterface {
    fn default() -> Self {
        Self {
            host: Url::parse(DEFAULT_HOST).unwrap(),
            tls: false,
            port: DEFAULT_PORT,
            tls_cert_path: None,
            tls_key_path: None,
        }
    }
}

impl TryFrom<&HttpSocketConfigFile> for HttpSocketInterface {
    type Error = Error;

    fn try_from(value: &HttpSocketConfigFile) -> Result<Self, Self::Error> {
        if !(value.port < u16::MAX as i64 && value.port > u16::MIN as i64) {
            return Err(Error::invalid_port_number(value.port));
        }
        let mut url = parse_url(value.host.as_str())?;
        if let Some(explict_tls) = value.tls {
            match explict_tls {
                true => url.set_scheme(HTTPS),
                false => url.set_scheme(HTTP),
            }
            .expect("TryFrom HttpSocketConfigFile Unable to set url scheme");
        }

        url.set_port(Some(value.port as u16)).unwrap();
        Ok(HttpSocketInterface::new(&url, value.tls_cert_path.as_ref(), value.tls_key_path.as_ref()))
    }
}

#[typetag::deserialize(name = "http")]
impl InterfaceConfig for HttpSocketConfigFile {
    fn to_interface(&self) -> Result<Box<dyn Interface + Send>, Error> {
        Ok(Box::new(HttpSocketInterface::try_from(self)?))
    }
}

#[async_trait]
impl Interface for HttpSocketInterface {
    #[cfg(feature = "http-server")]
    async fn receive(&self, interface_tx: mpsc::Sender<String>, shutdown: watch::Receiver<bool>) -> Result<(), Error> {
        use crate::interfaces::http::http_server::start_monitoring;

        if self.tls && (self.tls_cert_path().is_none() || self.tls_cert_path().is_none()) {
            Err(Error::invalid_interface_configuration(
                "Both tls_cert_path and tls_cert_path must be provided for a TLS server",
            ))
        } else {
            for socket in self.sockets()? {
                let tls = self.tls;
                let itx = interface_tx.clone();
                let srx = shutdown.clone();
                let cert_path = self.tls_cert_path.clone();
                let key_path = self.tls_key_path.clone();
                tokio::spawn(async move { start_monitoring(itx, srx, socket, tls, cert_path, key_path).await });
            }
            Ok(())
        }
    }

    #[cfg(not(feature = "http-server"))]
    async fn receive(
        &self,
        _interface_tx: mpsc::Sender<String>,
        _shutdown: watch::Receiver<bool>,
    ) -> Result<(), Error> {
        Err(Error::disabled_interface_feature("http-server".to_string()))
    }

    #[cfg(feature = "http-client")]
    async fn send(
        &self,
        interface_rx: broadcast::Receiver<Notification>,
        shutdown: watch::Receiver<bool>,
    ) -> Result<(), Error> {
        use crate::interfaces::http::http_client::start_sending;
        use tracing::debug;

        let mut url = self.host.clone();
        url.set_path(format!("{}/{}", BASE_PATH, NOTIFICATION_PATH).as_str());
        debug!("Sending notification to: {}", url.as_str());

        tokio::spawn(async move { start_sending(interface_rx, shutdown, url.as_str()).await });
        Ok(())
    }

    #[cfg(not(feature = "http-client"))]
    async fn send(
        &self,
        _interface_rx: broadcast::Receiver<Notification>,
        _shutdown: watch::Receiver<bool>,
    ) -> Result<(), Error> {
        Err(Error::disabled_interface_feature("http-client".to_string()))
    }
}

fn parse_url(value: &str) -> Result<Url, Error> {
    match Url::parse(value) {
        Ok(url) => Ok(url),
        Err(ParseError::RelativeUrlWithoutBase) => parse_url(format!("{}://{}", HTTP, value).as_str()),
        Err(error) => Err(error.into()),
    }
}