use crate::domain::{cert_status_from_expiry, Certificate, CERT_ERROR, CERT_UNKNOWN}; use crate::error::AppResult; use crate::repositories::{certificates, domains, subdomains}; use chrono::Utc; use sqlx::SqlitePool; use rustls::{ClientConfig, RootCertStore}; use rustls::pki_types::ServerName; use std::net::ToSocketAddrs; use std::sync::Arc; use tokio::net::TcpStream; use tokio::time::{timeout, Duration as TokioDuration}; use tokio_rustls::TlsConnector; use x509_parser::prelude::FromDer; pub async fn list_certificates(pool: &SqlitePool, status: Option<&str>) -> AppResult> { certificates::list(pool, status).await } pub async fn get_certificate(pool: &SqlitePool, id: i64) -> AppResult { certificates::get(pool, id).await } pub async fn check_hostname(hostname: &str) -> (Option>, Option) { let addr = match format!("{hostname}:443").to_socket_addrs() { Ok(mut addrs) => match addrs.next() { Some(a) => a, None => return (None, Some("cannot resolve host".into())), }, Err(e) => return (None, Some(e.to_string())), }; let stream = match timeout(TokioDuration::from_secs(10), TcpStream::connect(addr)).await { Ok(Ok(s)) => s, Ok(Err(e)) => return (None, Some(e.to_string())), Err(_) => return (None, Some("connection timeout".into())), }; let mut root_store = RootCertStore::empty(); root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); let config = ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(); let connector = TlsConnector::from(Arc::new(config)); let server_name = match ServerName::try_from(hostname.to_string()) { Ok(n) => n, Err(e) => return (None, Some(e.to_string())), }; let tls = match connector.connect(server_name, stream).await { Ok(s) => s, Err(e) => return (None, Some(e.to_string())), }; let (_, session) = tls.into_inner(); let certs = session.peer_certificates(); let Some(chain) = certs else { return (None, Some("no peer certificates".into())); }; let Some(leaf) = chain.first() else { return (None, Some("empty cert chain".into())); }; match x509_parser::certificate::X509Certificate::from_der(leaf.as_ref()) { Ok((_, cert)) => { let not_after = cert.validity().not_after.timestamp(); let expires = chrono::DateTime::from_timestamp(not_after, 0); (expires, None) } Err(e) => (None, Some(e.to_string())), } } pub async fn check_and_store( pool: &SqlitePool, domain_id: i64, subdomain_id: Option, hostname: &str, ) -> AppResult { let (expires_at, err) = check_hostname(hostname).await; let status = if let Some(err_msg) = &err { certificates::upsert_check( pool, domain_id, subdomain_id, hostname, expires_at.map(|e| e.to_rfc3339()).as_deref(), CERT_ERROR, Some(err_msg), ) .await? } else if let Some(exp) = expires_at { let days = (exp - Utc::now()).num_days(); let st = cert_status_from_expiry(days); certificates::upsert_check( pool, domain_id, subdomain_id, hostname, Some(&exp.to_rfc3339()), st, None, ) .await? } else { certificates::upsert_check( pool, domain_id, subdomain_id, hostname, None, CERT_UNKNOWN, Some("unknown expiry"), ) .await? }; Ok(status) } pub async fn run_all_checks(pool: &SqlitePool) -> AppResult { let mut count = 0usize; let all_domains = domains::list_all(pool).await?; for domain in all_domains { check_and_store(pool, domain.id, None, &domain.zone_name).await?; count += 1; } let subs = subdomains::list_all(pool).await?; for sub in subs { check_and_store(pool, sub.domain_id, Some(sub.id), &sub.fqdn).await?; count += 1; } Ok(count) } pub async fn status_summary(pool: &SqlitePool) -> AppResult> { certificates::count_by_status(pool).await }