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(),
});
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(())
}
}