domani/src/domain/checker.rs

131 lines
3.7 KiB
Rust
Raw Normal View History

use std::error::Error;
2023-05-15 16:23:53 +00:00
use std::net;
use std::str::FromStr;
use std::sync;
use crate::domain;
use trust_dns_client::client::{AsyncClient, ClientHandle};
use trust_dns_client::rr::{DNSClass, Name, RData, RecordType};
use trust_dns_client::udp;
#[derive(thiserror::Error, Debug)]
pub enum NewDNSCheckerError {
#[error("invalid resolver address")]
InvalidResolverAddress,
#[error(transparent)]
Unexpected(Box<dyn Error>),
}
#[derive(thiserror::Error, Debug)]
pub enum CheckDomainError {
2023-05-15 20:16:29 +00:00
#[error("target A not set")]
TargetANotSet,
#[error("challenge token not set")]
ChallengeTokenNotSet,
#[error(transparent)]
Unexpected(Box<dyn Error>),
}
pub struct DNSChecker {
2023-05-15 20:16:29 +00:00
target_a: net::Ipv4Addr,
// TODO we should use some kind of connection pool here, I suppose
client: tokio::sync::Mutex<AsyncClient>,
}
pub fn new(
tokio_runtime: sync::Arc<tokio::runtime::Runtime>,
2023-05-15 20:16:29 +00:00
target_a: net::Ipv4Addr,
resolver_addr: &str,
) -> Result<DNSChecker, NewDNSCheckerError> {
let resolver_addr = resolver_addr
.parse()
.map_err(|_| NewDNSCheckerError::InvalidResolverAddress)?;
let stream = udp::UdpClientStream::<tokio::net::UdpSocket>::new(resolver_addr);
let (client, bg) = tokio_runtime
.block_on(async { AsyncClient::connect(stream).await })
.map_err(|e| NewDNSCheckerError::Unexpected(Box::from(e)))?;
tokio_runtime.spawn(bg);
Ok(DNSChecker {
2023-05-15 20:16:29 +00:00
target_a,
client: tokio::sync::Mutex::new(client),
})
}
impl DNSChecker {
pub async fn check_domain(
&self,
domain: &domain::Name,
challenge_token: &str,
) -> Result<(), CheckDomainError> {
let domain = &domain.inner;
2023-05-15 16:23:53 +00:00
// check that the AAAA is installed correctly on the domain
{
let response = match self
.client
.lock()
.await
2023-05-15 21:01:24 +00:00
.query(domain.clone(), DNSClass::IN, RecordType::A)
.await
{
Ok(res) => res,
Err(e) => return Err(CheckDomainError::Unexpected(Box::from(e))),
};
let records = response.answers();
if records.len() != 1 {
2023-05-15 20:16:29 +00:00
return Err(CheckDomainError::TargetANotSet);
}
2023-05-15 20:16:29 +00:00
// if the single record isn't a A, or it's not the target A, then return
// TargetANAMENotSet
match records[0].data() {
2023-05-15 20:16:29 +00:00
Some(RData::A(remote_a)) if remote_a == &self.target_a => (),
_ => return Err(CheckDomainError::TargetANotSet),
}
}
// 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")
.map_err(|e| CheckDomainError::Unexpected(Box::from(e)))?
.append_domain(&domain)
.map_err(|e| CheckDomainError::Unexpected(Box::from(e)))?;
let response = match self
.client
.lock()
.await
.query(domain, DNSClass::IN, RecordType::TXT)
.await
{
Ok(res) => res,
Err(e) => return Err(CheckDomainError::Unexpected(Box::from(e))),
};
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(())
}
}