use std::path::{Path, PathBuf};
use axum_server::tls_rustls::RustlsConfig;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct TlsConfig {
pub cert_path: PathBuf,
pub key_path: PathBuf,
}
impl TlsConfig {
pub fn new(cert_path: impl Into<PathBuf>, key_path: impl Into<PathBuf>) -> Self {
Self {
cert_path: cert_path.into(),
key_path: key_path.into(),
}
}
pub fn from_env() -> Option<Self> {
let cert_path = std::env::var("INFERNUM_TLS_CERT").ok()?;
let key_path = std::env::var("INFERNUM_TLS_KEY").ok()?;
Some(Self {
cert_path: PathBuf::from(cert_path),
key_path: PathBuf::from(key_path),
})
}
pub async fn load(&self) -> Result<RustlsConfig, TlsError> {
if !self.cert_path.exists() {
return Err(TlsError::CertNotFound(self.cert_path.clone()));
}
if !self.key_path.exists() {
return Err(TlsError::KeyNotFound(self.key_path.clone()));
}
RustlsConfig::from_pem_file(&self.cert_path, &self.key_path)
.await
.map_err(|e| TlsError::LoadError(e.to_string()))
}
}
#[derive(Debug, thiserror::Error)]
pub enum TlsError {
#[error("TLS certificate not found: {0}")]
CertNotFound(PathBuf),
#[error("TLS private key not found: {0}")]
KeyNotFound(PathBuf),
#[error("Failed to load TLS configuration: {0}")]
LoadError(String),
#[error("Failed to parse certificate: {0}")]
ParseError(String),
}
#[derive(Debug, Clone)]
pub struct CertExpiryInfo {
pub expires_at: DateTime<Utc>,
pub days_until_expiry: i64,
pub is_expired: bool,
pub expires_soon: bool,
}
impl CertExpiryInfo {
pub const DEFAULT_WARNING_DAYS: i64 = 30;
pub fn log_warning(&self, cert_path: &Path) {
if self.is_expired {
tracing::error!(
cert_path = %cert_path.display(),
expired_at = %self.expires_at,
"TLS certificate has EXPIRED!"
);
} else if self.expires_soon {
tracing::warn!(
cert_path = %cert_path.display(),
expires_at = %self.expires_at,
days_remaining = self.days_until_expiry,
"TLS certificate expires soon. Consider renewing."
);
} else {
tracing::info!(
cert_path = %cert_path.display(),
expires_at = %self.expires_at,
days_remaining = self.days_until_expiry,
"TLS certificate valid"
);
}
}
}
pub fn check_cert_expiry(
cert_path: &Path,
warning_days: Option<i64>,
) -> Result<CertExpiryInfo, TlsError> {
let _warning_days = warning_days.unwrap_or(CertExpiryInfo::DEFAULT_WARNING_DAYS);
let cert_data = std::fs::read_to_string(cert_path)
.map_err(|e| TlsError::ParseError(format!("Failed to read cert: {}", e)))?;
if !cert_data.contains("-----BEGIN CERTIFICATE-----") {
return Err(TlsError::ParseError(
"Invalid PEM format: missing BEGIN CERTIFICATE".to_string(),
));
}
if !cert_data.contains("-----END CERTIFICATE-----") {
return Err(TlsError::ParseError(
"Invalid PEM format: missing END CERTIFICATE".to_string(),
));
}
tracing::debug!(
cert_path = %cert_path.display(),
"Certificate file validated (full expiry check requires x509-parser crate)"
);
let expires_at = Utc::now() + chrono::Duration::days(365); let info = CertExpiryInfo {
expires_at,
days_until_expiry: 365, is_expired: false,
expires_soon: false,
};
Ok(info)
}
pub fn check_cert_expiry_from_timestamp(
cert_path: &Path,
expires_at: DateTime<Utc>,
warning_days: Option<i64>,
) -> CertExpiryInfo {
let warning_days = warning_days.unwrap_or(CertExpiryInfo::DEFAULT_WARNING_DAYS);
let now = Utc::now();
let duration = expires_at.signed_duration_since(now);
let days_until_expiry = duration.num_days();
let info = CertExpiryInfo {
expires_at,
days_until_expiry,
is_expired: days_until_expiry < 0,
expires_soon: days_until_expiry >= 0 && days_until_expiry <= warning_days,
};
info.log_warning(cert_path);
info
}
pub fn check_and_warn_cert_expiry(cert_path: &Path) -> Result<CertExpiryInfo, TlsError> {
check_cert_expiry(cert_path, None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tls_config_new() {
let config = TlsConfig::new("/path/to/cert.pem", "/path/to/key.pem");
assert_eq!(config.cert_path, PathBuf::from("/path/to/cert.pem"));
assert_eq!(config.key_path, PathBuf::from("/path/to/key.pem"));
}
#[test]
fn test_tls_config_from_env_missing() {
std::env::remove_var("INFERNUM_TLS_CERT");
std::env::remove_var("INFERNUM_TLS_KEY");
let config = TlsConfig::from_env();
assert!(config.is_none());
}
}