use std::{fmt::Display, str::FromStr};
use anyhow::{Context, anyhow};
use cloudflare::endpoints::dns::dns::DnsContent;
use instant_acme::{
AuthorizationStatus, ChallengeType, Error as AcmeError, Identifier, LetsEncrypt, OrderStatus,
};
use strum::{Display, EnumString};
use url::Url;
#[derive(Clone, Display, EnumString, PartialEq, Eq)]
#[strum(serialize_all = "snake_case")]
pub enum Challenge {
Alpn,
Dns,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AcmeUrl {
LetsEncryptStaging,
LetsEncryptProduction,
Custom(Url),
}
impl Display for AcmeUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LetsEncryptStaging => write!(f, "{}", LetsEncrypt::Staging.url()),
Self::LetsEncryptProduction => write!(f, "{}", LetsEncrypt::Production.url()),
Self::Custom(v) => write!(f, "{v}"),
}
}
}
impl FromStr for AcmeUrl {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"le_prod" => Ok(Self::LetsEncryptProduction),
"le_stag" => Ok(Self::LetsEncryptStaging),
_ => Ok(Self::Custom(Url::parse(s).context("unable to parse URL")?)),
}
}
}
pub struct AcmeCert {
pub cert: Vec<u8>,
pub key: Vec<u8>,
}
#[derive(Clone, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
#[non_exhaustive]
pub enum DnsBackend {
Cloudflare,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Record {
Txt(String),
}
impl TryFrom<DnsContent> for Record {
type Error = anyhow::Error;
fn try_from(value: DnsContent) -> Result<Self, Self::Error> {
match value {
DnsContent::TXT { content } => Ok(Self::Txt(content)),
_ => Err(anyhow!("not supported")),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_acme_url() {
assert_eq!(
AcmeUrl::from_str("le_prod").unwrap(),
AcmeUrl::LetsEncryptProduction
);
assert_eq!(
AcmeUrl::from_str("le_stag").unwrap(),
AcmeUrl::LetsEncryptStaging
);
assert_eq!(
AcmeUrl::from_str("https://foo.bar/dir").unwrap(),
AcmeUrl::Custom("https://foo.bar/dir".parse().unwrap())
);
assert!(AcmeUrl::from_str("123#3##!").is_err());
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Unexpected authorization status: {0:?}")]
UnexpectedAuthorizationStatus(AuthorizationStatus),
#[error("Unexpected order status: {0:?}")]
UnexpectedOrderStatus(OrderStatus),
#[error("Unable to set challenge token: {0}")]
UnableToSetChallengeToken(anyhow::Error),
#[error("Unable to unset challenge token: {0}")]
UnableToUnsetChallengeToken(anyhow::Error),
#[error("Unable to verify challenge token: {0}")]
UnableToVerifyChallengeToken(anyhow::Error),
#[error("Unable to create order: {0}")]
UnableToCreateOrder(AcmeError),
#[error("Unable to get authorizations: {0}")]
UnableToGetAuthorizations(AcmeError),
#[error("Unable to set challenge as ready: {0}")]
UnableToSetChallengeReady(AcmeError),
#[error("Unable to finalize order: {0}")]
UnableToFinalizeOrder(AcmeError),
#[error("Unable to get certificate: {0}")]
UnableToGetCertificate(AcmeError),
#[error("Unable to generate certificate params: {0}")]
UnableToGenerateCertificateParams(rcgen::Error),
#[error("Unable to generate private key: {0}")]
UnableToGeneratePrivateKey(rcgen::Error),
#[error("Unable to parse private key: {0}")]
UnableToParsePrivateKey(rcgen::Error),
#[error("Unable to create CSR: {0}")]
UnableToCreateCSR(rcgen::Error),
#[error("No challenge found: {0:?}")]
NoChallengeFound(ChallengeType),
#[error("Unsupported identifier type: {0:?}")]
UnsupportedIdentifierType(Identifier),
#[error("Order unable to reach ready status: {0}")]
OrderUnableToReachReadyStatus(AcmeError),
#[error(transparent)]
Generic(#[from] anyhow::Error),
}
impl Error {
pub fn rate_limited(&self) -> bool {
let acme_error = match self {
Self::UnableToCreateOrder(v) => v,
Self::UnableToGetAuthorizations(v) => v,
Self::UnableToSetChallengeReady(v) => v,
Self::UnableToFinalizeOrder(v) => v,
Self::UnableToGetCertificate(v) => v,
Self::OrderUnableToReachReadyStatus(v) => v,
_ => return false,
};
if let AcmeError::Api(problem) = acme_error {
return problem.status == Some(429);
}
false
}
}