use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::str::FromStr;
use crate::error::{Result, ShoheError};
use crate::resolver::QueryOptions;
use hickory_proto::rr::RecordType;
pub use crate::resolver::{DnsQueryResult, DnsQuery, DnsRecord, RecordData, TrustState};
pub use crate::resolver::iterative::{ResolutionTrace, ResolutionStep, StepResponseType};
pub use crate::dnssec::chain::{DnssecChain, DnssecStep, DnssecStepType};
pub mod propagation;
pub mod email;
pub mod bench;
pub mod tls;
pub use propagation::{check_propagation, check_propagation_global, PropagationRequest, PropagationResolver, PropagationResult, PropagationStatus, ResolverCheckResult};
pub use email::{check_email_security, EmailSecurityRequest, EmailSecurityResult};
pub use bench::{benchmark_latency, LatencyBenchRequest, BenchTransport, LatencyBenchResult};
pub use tls::{check_tls_chain, TlsCheckRequest, TlsCheckResult, CertInfo, DaneTlsaResult};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "config")]
pub enum Transport {
#[serde(rename = "system")]
System,
#[serde(rename = "server")]
Server(String),
#[serde(rename = "doh")]
Doh(String),
#[serde(rename = "dot")]
Dot(String),
#[serde(rename = "doq")]
Doq(String),
}
impl Default for Transport {
fn default() -> Self { Transport::System }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsCheckRequest {
pub domain: String,
#[serde(default = "default_record_types")]
pub record_types: Vec<String>,
#[serde(default)]
pub transport: Transport,
#[serde(default)]
pub validate_dnssec: bool,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub ipv4_only: bool,
#[serde(default)]
pub ipv6_only: bool,
#[serde(default)]
pub no_recurse: bool,
#[serde(default)]
pub force_tcp: bool,
}
fn default_record_types() -> Vec<String> { vec!["A".to_string()] }
fn default_timeout() -> u64 { 5 }
impl Default for DnsCheckRequest {
fn default() -> Self {
Self {
domain: String::new(),
record_types: default_record_types(),
transport: Transport::default(),
validate_dnssec: false,
timeout_secs: default_timeout(),
ipv4_only: false,
ipv6_only: false,
no_recurse: false,
force_tcp: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnssecCheckRequest {
pub domain: String,
#[serde(default = "default_record_type")]
pub record_type: String,
#[serde(default)]
pub resolver_ip: Option<String>,
#[serde(default)]
pub verbose: bool,
}
fn default_record_type() -> String { "A".to_string() }
impl Default for DnssecCheckRequest {
fn default() -> Self {
Self {
domain: String::new(),
record_type: default_record_type(),
resolver_ip: None,
verbose: false,
}
}
}
pub async fn check_dns(req: &DnsCheckRequest) -> Result<Vec<DnsQueryResult>> {
if req.domain.is_empty() {
return Err(ShoheError::Parse("domain cannot be empty".to_string()));
}
let record_types = if req.record_types.is_empty() {
vec!["A".to_string()]
} else {
req.record_types.clone()
};
let transport = build_transport_config(&req.transport).await?;
let mut results = Vec::new();
let mut handles = vec![];
for rtype_str in record_types {
let rtype = RecordType::from_str(&rtype_str)
.map_err(|_| ShoheError::Parse(format!("invalid record type: {}", rtype_str)))?;
let opts = QueryOptions {
domain: req.domain.clone(),
record_type: rtype,
server: parse_server_addr(&req.transport),
transport: transport.clone(),
validate_dnssec: req.validate_dnssec,
force_tcp: req.force_tcp,
no_recurse: req.no_recurse,
timeout_secs: req.timeout_secs,
ipv4_only: req.ipv4_only,
ipv6_only: req.ipv6_only,
};
let handle = tokio::spawn(async move { crate::resolver::standard::query(&opts).await });
handles.push(handle);
}
for handle in handles {
let result = handle.await.map_err(|e| ShoheError::Transport(e.to_string()))??;
results.push(result);
}
Ok(results)
}
pub async fn check_dnssec(req: &DnssecCheckRequest) -> Result<DnssecChain> {
if req.domain.is_empty() {
return Err(ShoheError::Parse("domain cannot be empty".to_string()));
}
let rtype = RecordType::from_str(&req.record_type)
.map_err(|_| ShoheError::Parse(format!("invalid record type: {}", req.record_type)))?;
let resolver_ip = if let Some(ref ip_str) = req.resolver_ip {
Some(IpAddr::from_str(ip_str)
.map_err(|_| ShoheError::Parse(format!("invalid IP address: {}", ip_str)))?)
} else {
None
};
crate::dnssec::chain::build_chain(&req.domain, rtype, resolver_ip, req.verbose).await
}
pub async fn trace_resolution(domain: &str, record_type: &str) -> Result<ResolutionTrace> {
if domain.is_empty() {
return Err(ShoheError::Parse("domain cannot be empty".to_string()));
}
let rtype = RecordType::from_str(record_type)
.map_err(|_| ShoheError::Parse(format!("invalid record type: {}", record_type)))?;
crate::resolver::iterative::trace(domain, rtype, None).await
}
async fn build_transport_config(transport: &Transport) -> Result<Option<(hickory_resolver::config::ResolverConfig, String)>> {
match transport {
Transport::System => Ok(None),
Transport::Server(_) => Ok(None),
Transport::Doh(url) => {
let (config, label) = crate::transport::doh::build_doh_config(url).await
.map_err(|e| ShoheError::Transport(format!("DoH config failed: {}", e)))?;
Ok(Some((config, label)))
}
Transport::Dot(addr) => {
let (config, label) = crate::transport::dot::build_dot_config(addr).await
.map_err(|e| ShoheError::Transport(format!("DoT config failed: {}", e)))?;
Ok(Some((config, label)))
}
Transport::Doq(addr) => {
let (config, label) = crate::transport::doq::build_doq_config(addr).await
.map_err(|e| ShoheError::Transport(format!("DoQ config failed: {}", e)))?;
Ok(Some((config, label)))
}
}
}
fn parse_server_addr(transport: &Transport) -> Option<std::net::SocketAddr> {
match transport {
Transport::Server(addr_str) => {
std::net::SocketAddr::from_str(addr_str)
.or_else(|_| std::net::IpAddr::from_str(addr_str)
.map(|ip| std::net::SocketAddr::new(ip, 53)))
.ok()
}
_ => None,
}
}