use std::io;
use std::string::FromUtf8Error;
use domain::base::name::LongChainError;
use domain::base::wire::ParseError;
use domain::base::ToRelativeDname;
use domain::resolv::lookup::srv::SrvError;
use domain::{
base::{Dname, Question, RelativeDname, Rtype},
rdata::Txt,
resolv::StubResolver,
};
use http::uri::Scheme;
#[derive(Debug, Clone, Copy)]
pub enum DiscoverableService {
CalDavs,
CalDav,
CardDavs,
CardDav,
}
impl DiscoverableService {
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn relative_domain(self) -> &'static RelativeDname<[u8]> {
match self {
DiscoverableService::CalDavs => RelativeDname::from_slice(b"\x08_caldavs\x04_tcp"),
DiscoverableService::CalDav => RelativeDname::from_slice(b"\x07_caldav\x04_tcp"),
DiscoverableService::CardDavs => RelativeDname::from_slice(b"\x09_carddavs\x04_tcp"),
DiscoverableService::CardDav => RelativeDname::from_slice(b"\x08_carddav\x04_tcp"),
}
.expect("well known relative prefix is valid")
}
#[must_use]
pub fn scheme(self) -> Scheme {
match self {
DiscoverableService::CalDavs | DiscoverableService::CardDavs => Scheme::HTTPS,
DiscoverableService::CalDav | DiscoverableService::CardDav => Scheme::HTTP,
}
}
#[must_use]
pub fn well_known_path(self) -> &'static str {
match self {
DiscoverableService::CalDavs | DiscoverableService::CalDav => "/.well-known/caldav",
DiscoverableService::CardDavs | DiscoverableService::CardDav => "/.well-known/carddav",
}
}
#[must_use]
pub fn default_port(self) -> u16 {
match self {
DiscoverableService::CalDavs | DiscoverableService::CardDavs => 443,
DiscoverableService::CalDav | DiscoverableService::CardDav => 80,
}
}
#[must_use]
pub fn access_field(self) -> &'static str {
match self {
DiscoverableService::CalDavs | DiscoverableService::CardDavs => "calendar-access",
DiscoverableService::CalDav | DiscoverableService::CardDav => "addressbook",
}
}
}
pub async fn resolve_srv_record(
service: DiscoverableService,
domain: &Dname<impl AsRef<[u8]>>,
port: u16,
) -> Result<Option<Vec<(String, u16)>>, SrvError> {
Ok(StubResolver::new()
.lookup_srv(service.relative_domain(), domain, port)
.await?
.map(|found| {
found
.into_srvs()
.map(|entry| (entry.target().to_string(), entry.port()))
.collect()
}))
}
#[derive(thiserror::Error, Debug)]
pub enum TxtError {
#[error("I/O error performing DNS request")]
Network(#[from] io::Error),
#[error("the domain name is too long and cannot be queried")]
DomainTooLong(#[from] LongChainError),
#[error("error parsing DNS response")]
ParseError(#[from] ParseError),
#[error("txt record does not contain a valid utf-8 string")]
NotUtf8Error(#[from] FromUtf8Error),
#[error("data in txt record does no have the right syntax")]
BadTxt,
}
impl From<TxtError> for io::Error {
fn from(value: TxtError) -> Self {
match value {
TxtError::Network(err) => err,
TxtError::DomainTooLong(_) => io::Error::new(io::ErrorKind::InvalidInput, value),
TxtError::ParseError(_) | TxtError::NotUtf8Error(_) | TxtError::BadTxt => {
io::Error::new(io::ErrorKind::InvalidData, value)
}
}
}
}
pub async fn find_context_path_via_txt_records(
service: DiscoverableService,
domain: &Dname<impl AsRef<[u8]>>,
) -> Result<Option<String>, TxtError> {
let resolver = StubResolver::new();
let full_domain = service.relative_domain().chain(domain)?;
let question = Question::new_in(full_domain, Rtype::Txt);
let response = resolver.query(question).await?;
let Some(record) = response.answer()?.next() else {
return Ok(None);
};
let Some(parsed_record) = record?.into_record::<Txt<_>>()? else {
return Ok(None);
};
let bytes = parsed_record.data().text::<Vec<u8>>();
let path_result = String::from_utf8(bytes)?
.strip_prefix("path=")
.ok_or(TxtError::BadTxt)
.map(String::from);
Some(path_result).transpose()
}