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
139 lines
4.3 KiB
Rust
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
|
|
}
|