use std::io;
use std::net::SocketAddr;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TlsError {
#[error("Connection timeout after {duration:?} to {addr}")]
ConnectionTimeout {
duration: Duration,
addr: SocketAddr,
},
#[error("Connection refused by {addr}")]
ConnectionRefused { addr: SocketAddr },
#[error("DNS resolution failed for {hostname}: {source}")]
DnsResolutionFailed {
hostname: String,
#[source]
source: io::Error,
},
#[error("Protocol {protocol} not supported by server")]
ProtocolNotSupported { protocol: String },
#[error("Invalid TLS handshake: {details}")]
InvalidHandshake { details: String },
#[error("Certificate validation failed: {0}")]
CertificateError(#[from] CertificateValidationError),
#[error("I/O error: {source}")]
IoError {
#[from]
source: io::Error,
},
#[error("HTTP error (status {status}): {details}")]
HttpError { status: u16, details: String },
#[error("Parse error: {message}")]
ParseError { message: String },
#[error("OpenSSL error: {0}")]
OpenSslError(#[from] openssl::error::ErrorStack),
#[error("HTTP request failed: {0}")]
RequestError(#[from] reqwest::Error),
#[error("Invalid URL: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("STARTTLS protocol error ({protocol}): {details}")]
StarttlsError { protocol: String, details: String },
#[error("Cipher suite error: {message}")]
CipherError { message: String },
#[error("Invalid configuration: {message}")]
ConfigError { message: String },
#[error("Operation timed out after {duration:?}")]
Timeout { duration: Duration },
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Unexpected server response: {details}")]
UnexpectedResponse { details: String },
#[error("Connection closed by server: {details}")]
ConnectionClosed { details: String },
#[error("Invalid input: {message}")]
InvalidInput { message: String },
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("UTF-8 conversion error: {0}")]
Utf8Error(#[from] std::string::FromUtf8Error),
#[error("Integer parse error: {0}")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("File system error: {path}: {source}")]
FileSystemError {
path: String,
#[source]
source: io::Error,
},
#[error("mTLS configuration error: {message}")]
MtlsError { message: String },
#[error("PEM parsing error: {0}")]
PemError(#[from] pem::PemError),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Error)]
pub enum CertificateValidationError {
#[error("Certificate expired on {expiry_date}")]
Expired { expiry_date: String },
#[error("Certificate not valid until {valid_from}")]
NotYetValid { valid_from: String },
#[error("Hostname {hostname} does not match certificate (expected: {expected})")]
HostnameMismatch { hostname: String, expected: String },
#[error("Invalid certificate chain: {reason}")]
InvalidChain { reason: String },
#[error("Certificate is self-signed")]
SelfSigned,
#[error("Certificate has been revoked")]
Revoked,
#[error("Untrusted root CA: {issuer}")]
UntrustedRoot { issuer: String },
#[error("Invalid certificate signature")]
InvalidSignature,
#[error("Weak key size: {bits} bits (minimum: {minimum})")]
WeakKeySize { bits: usize, minimum: usize },
#[error("Certificate parsing error: {details}")]
ParseError { details: String },
}
impl From<anyhow::Error> for TlsError {
fn from(err: anyhow::Error) -> Self {
TlsError::Other(err.to_string())
}
}
impl From<std::str::Utf8Error> for TlsError {
fn from(err: std::str::Utf8Error) -> Self {
TlsError::ParseError {
message: format!("UTF-8 string error: {}", err),
}
}
}
impl From<tokio::time::error::Elapsed> for TlsError {
fn from(_err: tokio::time::error::Elapsed) -> Self {
TlsError::ConnectionTimeout {
duration: std::time::Duration::from_secs(0), addr: std::net::SocketAddr::from(([0, 0, 0, 0], 0)),
}
}
}
impl<S: std::fmt::Debug> From<openssl::ssl::HandshakeError<S>> for TlsError {
fn from(err: openssl::ssl::HandshakeError<S>) -> Self {
TlsError::InvalidHandshake {
details: format!("SSL handshake error: {}", err),
}
}
}
impl From<tokio::task::JoinError> for TlsError {
fn from(err: tokio::task::JoinError) -> Self {
TlsError::IoError {
source: std::io::Error::other(format!("Task join error: {}", err)),
}
}
}
macro_rules! impl_from_error {
($error_type:ty, $prefix:literal) => {
impl From<$error_type> for TlsError {
fn from(err: $error_type) -> Self {
TlsError::Other(format!(concat!($prefix, ": {}"), err))
}
}
};
}
impl_from_error!(csv::Error, "CSV error");
impl_from_error!(handlebars::RenderError, "Template render error");
impl_from_error!(lettre::address::AddressError, "Email address error");
impl_from_error!(lettre::error::Error, "Email error");
impl_from_error!(lettre::transport::smtp::Error, "SMTP error");
impl_from_error!(toml::de::Error, "TOML deserialization error");
impl_from_error!(toml::ser::Error, "TOML serialization error");
impl<W> From<csv::IntoInnerError<W>> for TlsError {
fn from(err: csv::IntoInnerError<W>) -> Self {
TlsError::IoError {
source: std::io::Error::other(format!("CSV writer error: {}", err)),
}
}
}
#[macro_export]
macro_rules! tls_bail {
($msg:literal $(,)?) => {
return Err($crate::error::TlsError::Other($msg.to_string()))
};
($fmt:expr, $($arg:tt)*) => {
return Err($crate::error::TlsError::Other(format!($fmt, $($arg)*)))
};
}
#[macro_export]
macro_rules! cert_error {
($variant:ident { $($field:ident: $value:expr),* $(,)? }) => {
$crate::error::TlsError::CertificateError(
$crate::error::CertificateValidationError::$variant { $($field: $value),* }
)
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn test_connection_timeout_error() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 443);
let err = TlsError::ConnectionTimeout {
duration: Duration::from_secs(5),
addr,
};
let msg = err.to_string();
assert!(msg.contains("timeout"));
assert!(msg.contains("127.0.0.1:443"));
}
#[test]
fn test_dns_resolution_failed() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "host not found");
let err = TlsError::DnsResolutionFailed {
hostname: "invalid.example".to_string(),
source: io_err,
};
let msg = err.to_string();
assert!(msg.contains("DNS resolution failed"));
assert!(msg.contains("invalid.example"));
}
#[test]
fn test_certificate_expired() {
let cert_err = CertificateValidationError::Expired {
expiry_date: "2023-01-01".to_string(),
};
let err = TlsError::CertificateError(cert_err);
let msg = err.to_string();
assert!(msg.contains("expired"));
assert!(msg.contains("2023-01-01"));
}
#[test]
fn test_certificate_hostname_mismatch() {
let cert_err = CertificateValidationError::HostnameMismatch {
hostname: "example.com".to_string(),
expected: "*.example.org".to_string(),
};
let msg = cert_err.to_string();
assert!(msg.contains("example.com"));
assert!(msg.contains("*.example.org"));
}
#[test]
fn test_error_conversion_from_io() {
let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "refused");
let tls_err: TlsError = io_err.into();
assert!(matches!(tls_err, TlsError::IoError { .. }));
}
#[test]
fn test_error_chain_preserved() {
use std::error::Error;
let io_err = io::Error::new(io::ErrorKind::NotFound, "dns failed");
let err = TlsError::DnsResolutionFailed {
hostname: "test.example".to_string(),
source: io_err,
};
assert!(err.source().is_some());
}
}