Files
cloudflare-domain-manager/backend/src/services/certificate_service.rs
T
Denozordec 87789fa82d
Build, Test, and Push CFDM Docker Image / test (push) Failing after 1m51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
INIT COMMIT
2026-06-13 16:43:52 +07:00

139 lines
4.3 KiB
Rust

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<Vec<Certificate>> {
certificates::list(pool, status).await
}
pub async fn get_certificate(pool: &SqlitePool, id: i64) -> AppResult<Certificate> {
certificates::get(pool, id).await
}
pub async fn check_hostname(hostname: &str) -> (Option<chrono::DateTime<Utc>>, Option<String>) {
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<i64>,
hostname: &str,
) -> AppResult<Certificate> {
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<usize> {
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<Vec<(String, i64)>> {
certificates::count_by_status(pool).await
}