2023-05-07 15:06:51 +00:00
|
|
|
use std::error::Error;
|
|
|
|
use std::str::FromStr;
|
2023-05-14 09:18:36 +00:00
|
|
|
use std::sync;
|
2023-05-07 15:06:51 +00:00
|
|
|
|
2023-05-12 13:19:24 +00:00
|
|
|
use crate::domain;
|
|
|
|
|
2023-05-07 16:07:31 +00:00
|
|
|
use mockall::automock;
|
2023-05-14 09:18:36 +00:00
|
|
|
use trust_dns_client::client::{AsyncClient, ClientHandle};
|
2023-05-07 15:06:51 +00:00
|
|
|
use trust_dns_client::rr::{DNSClass, Name, RData, RecordType};
|
2023-05-14 09:18:36 +00:00
|
|
|
use trust_dns_client::udp;
|
2023-05-07 15:06:51 +00:00
|
|
|
|
2023-05-09 11:44:57 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
2023-05-07 15:06:51 +00:00
|
|
|
pub enum NewDNSCheckerError {
|
2023-05-09 11:44:57 +00:00
|
|
|
#[error("invalid resolver address")]
|
2023-05-07 15:06:51 +00:00
|
|
|
InvalidResolverAddress,
|
2023-05-09 11:44:57 +00:00
|
|
|
|
|
|
|
#[error("invalid target CNAME")]
|
2023-05-07 15:06:51 +00:00
|
|
|
InvalidTargetCNAME,
|
|
|
|
|
2023-05-09 11:44:57 +00:00
|
|
|
#[error(transparent)]
|
|
|
|
Unexpected(Box<dyn Error>),
|
2023-05-07 15:06:51 +00:00
|
|
|
}
|
|
|
|
|
2023-05-09 11:44:57 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
2023-05-07 15:06:51 +00:00
|
|
|
pub enum CheckDomainError {
|
2023-05-09 11:44:57 +00:00
|
|
|
#[error("target CNAME not set")]
|
2023-05-07 15:06:51 +00:00
|
|
|
TargetCNAMENotSet,
|
2023-05-09 11:44:57 +00:00
|
|
|
|
|
|
|
#[error("challenge token not set")]
|
2023-05-07 15:06:51 +00:00
|
|
|
ChallengeTokenNotSet,
|
|
|
|
|
2023-05-09 11:44:57 +00:00
|
|
|
#[error(transparent)]
|
|
|
|
Unexpected(Box<dyn Error>),
|
2023-05-07 15:06:51 +00:00
|
|
|
}
|
|
|
|
|
2023-05-07 15:17:04 +00:00
|
|
|
#[automock]
|
2023-05-12 14:43:28 +00:00
|
|
|
pub trait Checker: std::marker::Send + std::marker::Sync {
|
2023-05-12 13:19:24 +00:00
|
|
|
fn check_domain(
|
|
|
|
&self,
|
|
|
|
domain: &domain::Name,
|
|
|
|
challenge_token: &str,
|
|
|
|
) -> Result<(), CheckDomainError>;
|
2023-05-07 15:06:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct DNSChecker {
|
2023-05-14 09:18:36 +00:00
|
|
|
tokio_runtime: sync::Arc<tokio::runtime::Runtime>,
|
2023-05-07 15:06:51 +00:00
|
|
|
target_cname: Name,
|
2023-05-14 09:18:36 +00:00
|
|
|
|
|
|
|
// TODO we should use some kind of connection pool here, I suppose
|
|
|
|
client: tokio::sync::Mutex<AsyncClient>,
|
2023-05-07 15:06:51 +00:00
|
|
|
}
|
|
|
|
|
2023-05-12 13:19:24 +00:00
|
|
|
pub fn new(
|
2023-05-14 09:18:36 +00:00
|
|
|
tokio_runtime: sync::Arc<tokio::runtime::Runtime>,
|
2023-05-12 13:19:24 +00:00
|
|
|
target_cname: domain::Name,
|
|
|
|
resolver_addr: &str,
|
|
|
|
) -> Result<impl Checker, NewDNSCheckerError> {
|
2023-05-11 12:19:36 +00:00
|
|
|
let resolver_addr = resolver_addr
|
|
|
|
.parse()
|
|
|
|
.map_err(|_| NewDNSCheckerError::InvalidResolverAddress)?;
|
2023-05-07 15:06:51 +00:00
|
|
|
|
2023-05-14 09:18:36 +00:00
|
|
|
let stream = udp::UdpClientStream::<tokio::net::UdpSocket>::new(resolver_addr);
|
|
|
|
|
|
|
|
let (client, bg) = tokio_runtime
|
|
|
|
.block_on(async { AsyncClient::connect(stream).await })
|
2023-05-11 12:19:36 +00:00
|
|
|
.map_err(|e| NewDNSCheckerError::Unexpected(Box::from(e)))?;
|
2023-05-09 11:44:57 +00:00
|
|
|
|
2023-05-14 09:18:36 +00:00
|
|
|
tokio_runtime.spawn(bg);
|
2023-05-07 15:06:51 +00:00
|
|
|
|
2023-05-11 12:19:36 +00:00
|
|
|
Ok(DNSChecker {
|
2023-05-14 09:18:36 +00:00
|
|
|
tokio_runtime,
|
2023-05-12 13:19:24 +00:00
|
|
|
target_cname: target_cname.inner,
|
2023-05-14 09:18:36 +00:00
|
|
|
client: tokio::sync::Mutex::new(client),
|
2023-05-11 12:19:36 +00:00
|
|
|
})
|
2023-05-07 15:06:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Checker for DNSChecker {
|
2023-05-12 13:19:24 +00:00
|
|
|
fn check_domain(
|
|
|
|
&self,
|
|
|
|
domain: &domain::Name,
|
|
|
|
challenge_token: &str,
|
|
|
|
) -> Result<(), CheckDomainError> {
|
|
|
|
let domain = &domain.inner;
|
2023-05-07 15:06:51 +00:00
|
|
|
|
|
|
|
// check that the CNAME is installed correctly on the domain
|
|
|
|
{
|
2023-05-14 09:18:36 +00:00
|
|
|
let response = match self.tokio_runtime.block_on(async {
|
|
|
|
self.client
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.query(domain.clone(), DNSClass::IN, RecordType::CNAME)
|
|
|
|
.await
|
|
|
|
}) {
|
|
|
|
Ok(res) => res,
|
|
|
|
Err(e) => return Err(CheckDomainError::Unexpected(Box::from(e))),
|
|
|
|
};
|
2023-05-07 15:06:51 +00:00
|
|
|
|
|
|
|
let records = response.answers();
|
|
|
|
|
|
|
|
if records.len() != 1 {
|
|
|
|
return Err(CheckDomainError::TargetCNAMENotSet);
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the single record isn't a CNAME, or it's not the target CNAME, then return
|
|
|
|
// TargetCNAMENotSet
|
|
|
|
match records[0].data() {
|
|
|
|
Some(RData::CNAME(remote_cname)) if remote_cname == &self.target_cname => (),
|
|
|
|
_ => return Err(CheckDomainError::TargetCNAMENotSet),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// check that the TXT record with the challenge token is correctly installed on the domain
|
|
|
|
{
|
2023-05-13 14:34:51 +00:00
|
|
|
let domain = Name::from_str("_domiply_challenge")
|
2023-05-09 11:44:57 +00:00
|
|
|
.map_err(|e| CheckDomainError::Unexpected(Box::from(e)))?
|
2023-05-14 09:18:36 +00:00
|
|
|
.append_domain(&domain)
|
2023-05-09 11:44:57 +00:00
|
|
|
.map_err(|e| CheckDomainError::Unexpected(Box::from(e)))?;
|
2023-05-09 11:52:21 +00:00
|
|
|
|
2023-05-14 09:18:36 +00:00
|
|
|
let response = match self.tokio_runtime.block_on(async {
|
|
|
|
self.client
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.query(domain, DNSClass::IN, RecordType::TXT)
|
|
|
|
.await
|
|
|
|
}) {
|
|
|
|
Ok(res) => res,
|
|
|
|
Err(e) => return Err(CheckDomainError::Unexpected(Box::from(e))),
|
|
|
|
};
|
2023-05-07 15:06:51 +00:00
|
|
|
|
|
|
|
let records = response.answers();
|
|
|
|
|
|
|
|
if !records.iter().any(|record| -> bool {
|
|
|
|
match record.data() {
|
|
|
|
Some(RData::TXT(txt)) => txt.to_string().contains(challenge_token),
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}) {
|
|
|
|
return Err(CheckDomainError::ChallengeTokenNotSet);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|