INIT COMMIT
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
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
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user