use crate::{DnsConfig, Error, Result, VerificationMode};
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
use trust_dns_resolver::TokioAsyncResolver;
pub async fn discover_dns_config(domain: &str) -> Result<DnsConfig> {
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
let lookup_domain = format!("_delve.{}", domain);
let txt_records = resolver
.txt_lookup(&lookup_domain)
.await
.map_err(|e| Error::Dns(format!("Failed to lookup {}: {}", lookup_domain, e)))?;
for record in txt_records.iter() {
let txt_data = record
.iter()
.map(|data| String::from_utf8_lossy(data.as_ref()))
.collect::<String>();
if txt_data.starts_with("v=delve") {
return parse_dns_record(&txt_data);
}
}
Err(Error::InvalidDnsRecord(format!(
"No valid DelVe TXT record found for {}",
lookup_domain
)))
}
fn parse_dns_record(record: &str) -> Result<DnsConfig> {
let mut version = None;
let mut mode = None;
let mut endpoint = None;
let mut public_key = None;
for part in record.split(';') {
let part = part.trim();
if let Some((key, value)) = part.split_once('=') {
match key.trim() {
"v" => version = Some(value.trim().to_string()),
"mode" => mode = Some(value.trim().to_string()),
"endpoint" => endpoint = Some(value.trim().to_string()),
"key" => public_key = Some(value.trim().to_string()),
_ => {} }
}
}
let version = version.ok_or_else(|| Error::MissingField("v".to_string()))?;
if !version.starts_with("delve0.") {
return Err(Error::UnsupportedVersion(version));
}
let mode_str = mode.ok_or_else(|| Error::MissingField("mode".to_string()))?;
let mode = match mode_str.as_str() {
"delegate" => VerificationMode::Delegate,
"direct" => VerificationMode::Direct,
_ => return Err(Error::InvalidMode(mode_str)),
};
let public_key = public_key.ok_or_else(|| Error::MissingField("key".to_string()))?;
if mode == VerificationMode::Delegate && endpoint.is_none() {
return Err(Error::MissingField("endpoint".to_string()));
}
Ok(DnsConfig {
version,
mode,
endpoint,
public_key,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_delegate_mode() {
let record = "v=delve0.1; mode=delegate; endpoint=https://verify.example.org; key=MCowBQYDK2VwAyEAXXXX";
let config = parse_dns_record(record).unwrap();
assert_eq!(config.version, "delve0.1");
assert_eq!(config.mode, VerificationMode::Delegate);
assert_eq!(
config.endpoint,
Some("https://verify.example.org".to_string())
);
assert_eq!(config.public_key, "MCowBQYDK2VwAyEAXXXX");
}
#[test]
fn test_parse_direct_mode() {
let record = "v=delve0.1; mode=direct; key=MCowBQYDK2VwAyEAYYYY";
let config = parse_dns_record(record).unwrap();
assert_eq!(config.version, "delve0.1");
assert_eq!(config.mode, VerificationMode::Direct);
assert_eq!(config.endpoint, None);
assert_eq!(config.public_key, "MCowBQYDK2VwAyEAYYYY");
}
#[test]
fn test_parse_missing_version() {
let record = "mode=delegate; endpoint=https://verify.example.org; key=XXXX";
assert!(matches!(
parse_dns_record(record),
Err(Error::MissingField(_))
));
}
#[test]
fn test_parse_invalid_version() {
let record = "v=delve1.0; mode=delegate; endpoint=https://verify.example.org; key=XXXX";
assert!(matches!(
parse_dns_record(record),
Err(Error::UnsupportedVersion(_))
));
}
#[test]
fn test_parse_delegate_missing_endpoint() {
let record = "v=delve0.1; mode=delegate; key=XXXX";
assert!(matches!(
parse_dns_record(record),
Err(Error::MissingField(_))
));
}
#[test]
fn test_parse_invalid_mode() {
let record = "v=delve0.1; mode=invalid; key=XXXX";
assert!(matches!(
parse_dns_record(record),
Err(Error::InvalidMode(_))
));
}
#[test]
fn test_to_dns_record_delegate() {
let config = DnsConfig {
version: "delve0.1".to_string(),
mode: VerificationMode::Delegate,
endpoint: Some("https://verify.example.org".to_string()),
public_key: "MCowBQYDK2VwAyEAXXXX".to_string(),
};
let record = config.to_dns_record();
assert_eq!(
record,
"v=delve0.1; mode=delegate; endpoint=https://verify.example.org; key=MCowBQYDK2VwAyEAXXXX"
);
let parsed = parse_dns_record(&record).unwrap();
assert_eq!(parsed.version, config.version);
assert_eq!(parsed.mode, config.mode);
assert_eq!(parsed.endpoint, config.endpoint);
assert_eq!(parsed.public_key, config.public_key);
}
#[test]
fn test_to_dns_record_direct() {
let config = DnsConfig {
version: "delve0.1".to_string(),
mode: VerificationMode::Direct,
endpoint: None,
public_key: "MCowBQYDK2VwAyEAYYYY".to_string(),
};
let record = config.to_dns_record();
assert_eq!(record, "v=delve0.1; mode=direct; key=MCowBQYDK2VwAyEAYYYY");
let parsed = parse_dns_record(&record).unwrap();
assert_eq!(parsed.version, config.version);
assert_eq!(parsed.mode, config.mode);
assert_eq!(parsed.endpoint, config.endpoint);
assert_eq!(parsed.public_key, config.public_key);
}
}