dat 4.2.2

DAT - Distributed Access Token
Documentation
use crate::dat::Dat;
use crate::error::DatError;
use crate::manager::DatManager;
use crate::payload::DatPayload;
use reqwest::Client;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;

pub static DAT_CMS_API_VERSION: &str = "v1";

pub struct DatCmsManager {
    url: String,
    token: String,
    version: RwLock<u64>,
    manager: DatManager,
    client: Client,
}

pub struct DatCmsManagerBuilder {
    https: bool,
    host: String,
    port: u16,
    token: String,
    verify_only: bool,
    interval: Duration,
}
impl DatCmsManagerBuilder {

    #[inline]
    pub fn https(mut self, https: bool) -> Self {
        self.https = https;
        self
    }

    #[inline]
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.host = host.into();
        self
    }

    #[inline]
    pub fn token(mut self, token: impl Into<String>) -> Self {
        self.token = token.into();
        self
    }

    #[inline]
    pub fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }

    #[inline]
    pub fn verify_only(mut self, verify_only: bool) -> Self {
        self.verify_only = verify_only;
        self
    }

    #[inline]
    pub fn interval(mut self, interval: Duration) -> Self {
        self.interval = interval;
        self
    }

    #[inline]
    pub fn interval_off(self) -> Self {
        self.interval(Duration::from_secs(0))
    }

    pub async fn build(self) -> Arc<DatCmsManager> {

        if self.host.is_empty() {
            panic!("host is empty");
        } else if self.host.contains('/') {
            panic!("invalid host: example: localhost, 192.168.1.1, dat.saro.me");
        } else if self.port == 0 {
            panic!("port is empty");
        }

        let proto = if self.https { "https" } else { "http" };
        let host = self.host;
        let port = self.port;
        let path_verify_only = if self.verify_only { "/verify-only" } else { "" };

        let url = format!("{proto}://{host}:{port}/{DAT_CMS_API_VERSION}/certs{path_verify_only}");

        let manager = Arc::new(DatCmsManager {
            url,
            token: self.token,
            version: RwLock::new(0),
            manager: DatManager::new(),
            client: Client::new(),
        });

        // first sync
        manager.sync().await.unwrap();

        let manager_clone = Arc::clone(&manager);

        if self.interval.as_secs() > 0 {
            tokio::spawn(async move {
                let mut ticker = tokio::time::interval(self.interval);
                loop {
                    ticker.tick().await;
                    let _ = manager_clone.sync().await.is_ok();
                }
            });
        } else {
            tracing::debug!("cms auto sync disabled");
        }

        manager
    }
}


impl Default for DatCmsManagerBuilder {
    fn default() -> Self {
        DatCmsManagerBuilder {
            https: false,
            host: "localhost".to_string(),
            port: 8088,
            token: "".to_string(),
            verify_only: false,
            interval: Duration::from_secs(60),
        }
    }
}

impl DatCmsManager {
    pub fn builder() -> DatCmsManagerBuilder {
        DatCmsManagerBuilder::default()
    }

    #[inline]
    pub fn issue(&self, plain: &str, secure: &str) -> Result<String, DatError> {
        self.manager.issue(plain, secure)
    }

    #[inline]
    pub fn parse(&self, dat: Dat) -> Result<DatPayload, DatError> {
        self.manager.parse(dat)
    }

    #[inline]
    pub fn parse_without_verify(&self, dat: Dat) -> Result<DatPayload, DatError> {
        self.manager.parse_without_verify(dat)
    }

    #[inline]
    pub fn get_manager(&self) -> &DatManager {
        &self.manager
    }

    #[inline]
    pub async fn get_version(&self) -> u64 {
        self.version.read().await.clone()
    }

    pub async fn sync(&self) -> Result<(), String> {
        let mut version_lock = self.version.try_write()
            .map_err(|_| format!("Last request ignored (Duplicate request) {} ", self.url))
            .inspect_err(|e| {
                #[cfg(feature = "tracing")]
                tracing::error!("[WARN] DAT CMS SYNC Drop: {e}")
            })?;

        let version = *version_lock;

        let response = self.client.get(&self.url)
            .query(&[("version", version)])
            .header("Authorization", &self.token)
            .send().await
            .map_err(|e| e.to_string())
            .inspect_err(|e| {
                #[cfg(feature = "tracing")]
                tracing::error!("[CRITICAL] DAT CMS SYNC Exception: {e}")
            })?;

        let res = response.error_for_status()
            .map_err(|e| e.to_string())
            .inspect_err(|e| {
                #[cfg(feature = "tracing")]
                tracing::error!("[CRITICAL] DAT CMS SYNC Exception: {e}");
            })?;

        let cert_str = res.text().await
            .map_err(|e| e.to_string())
            .inspect_err(|e| {
                #[cfg(feature = "tracing")]
                tracing::error!("[CRITICAL] DAT CMS SYNC Exception: {e}")
            })?;

        let mut split = cert_str.splitn(2, "\n");
        let ver = split.next()
            .ok_or_else(|| format!("empty response {}?version={}: {cert_str}", self.url, version))?;

        let certs = split.next().unwrap_or("").trim();
        if certs.is_empty() {
            #[cfg(feature = "tracing")]
            tracing::debug!("no new certificates in response {}?version={}: {cert_str}", self.url, version);
            return Ok(());
        }

        let ver = ver.parse::<u64>()
            .map_err(|_| format!("invalid version {}?version={}: {ver}", self.url, version))
            .inspect_err(|e| {
                #[cfg(feature = "tracing")]
                tracing::error!("[CRITICAL] DAT CMS SYNC Exception: {e}")
            })?;

        let count = self.manager.import(&certs, true)
            .map_err(|e| format!("import error {}: {e}", self.url))
            .inspect_err(|e| {
                #[cfg(feature = "tracing")]
                tracing::error!("[CRITICAL] DAT CMS SYNC Exception: {e}")
            })?;
        *version_lock = ver;

        #[cfg(feature = "tracing")]
        tracing::info!("Sync OK: Renew {} DAT certificates.", count);
        Ok(())
    }
}