1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct ValidationResult {
5 pub domain: String,
6 pub valid: bool,
7 pub skip_reason: Option<String>,
8 pub dns_valid: bool,
9 pub http_valid: bool,
10 pub ssl_valid: bool,
11 pub dns_info: Option<DnsValidation>,
12 pub http_info: Option<HttpValidation>,
13 pub ssl_info: Option<SslValidation>,
14 pub errors: Vec<String>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DnsValidation {
19 pub ip_addresses: Vec<String>,
20 pub mx_exists: bool,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct HttpValidation {
25 pub http_reachable: bool,
26 pub https_reachable: bool,
27 pub http_status: Option<u16>,
28 pub https_status: Option<u16>,
29 pub redirects_to_https: bool,
30 pub response_time_ms: u128,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SslValidation {
35 pub ssl_available: bool,
36 pub protocol_version: String,
37 pub cipher_suite: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ValidationStats {
42 pub total: usize,
43 pub valid: usize,
44 pub invalid: usize,
45 pub skipped: usize,
46 pub dns_failed: usize,
47 pub http_failed: usize,
48 pub ssl_failed: usize,
49 pub success_rate: f64,
50 pub processing_time_secs: f64,
51 pub domains_per_sec: f64,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct BulkValidationResult {
56 pub stats: ValidationStats,
57 pub valid_domains: Vec<String>,
58 pub results: Vec<ValidationResult>,
59}
60
61use hickory_resolver::config::*;
62use hickory_resolver::AsyncResolver;
63use reqwest::Client;
64use std::time::{Duration, Instant};
65use tokio::task::JoinSet;
66
67pub async fn validate_domain(domain: &str) -> ValidationResult {
68 let mut result = ValidationResult {
69 domain: domain.to_string(),
70 valid: false,
71 skip_reason: None,
72 dns_valid: false,
73 http_valid: false,
74 ssl_valid: false,
75 dns_info: None,
76 http_info: None,
77 ssl_info: None,
78 errors: vec![],
79 };
80
81 let mut dns_info = DnsValidation {
83 ip_addresses: vec![],
84 mx_exists: false,
85 };
86
87 let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default());
88
89 if let Ok(response) = resolver.ipv4_lookup(domain).await {
90 for ip in response.iter() {
91 dns_info.ip_addresses.push(ip.to_string());
92 }
93 }
94
95 if let Ok(response) = resolver.ipv6_lookup(domain).await {
96 for ip in response.iter() {
97 dns_info.ip_addresses.push(ip.to_string());
98 }
99 }
100
101 if let Ok(response) = resolver.mx_lookup(domain).await {
102 dns_info.mx_exists = response.iter().next().is_some();
103 }
104
105 result.dns_valid = !dns_info.ip_addresses.is_empty();
106 result.dns_info = Some(dns_info);
107
108 if !result.dns_valid {
109 result.errors.push("DNS Resolution failed. No A/AAAA records.".into());
110 return result; }
112
113 let start_http = Instant::now();
115 let client = Client::builder()
116 .timeout(Duration::from_secs(5))
117 .danger_accept_invalid_certs(true)
118 .redirect(reqwest::redirect::Policy::limited(3))
119 .build()
120 .unwrap_or_else(|_| Client::new());
121
122 let mut http_info = HttpValidation {
123 http_reachable: false,
124 https_reachable: false,
125 http_status: None,
126 https_status: None,
127 redirects_to_https: false,
128 response_time_ms: 0,
129 };
130
131 if let Ok(resp) = client.get(&format!("http://{}", domain)).send().await {
132 http_info.http_reachable = true;
133 http_info.http_status = Some(resp.status().as_u16());
134 if resp.url().scheme() == "https" {
135 http_info.redirects_to_https = true;
136 }
137 }
138
139 if let Ok(resp) = client.get(&format!("https://{}", domain)).send().await {
140 http_info.https_reachable = true;
141 http_info.https_status = Some(resp.status().as_u16());
142 }
143
144 http_info.response_time_ms = start_http.elapsed().as_millis();
145 result.http_valid = http_info.http_reachable || http_info.https_reachable;
146
147 if !result.http_valid {
148 result.errors.push("Host is unreachable via HTTP/HTTPS.".into());
149 }
150
151 result.http_info = Some(http_info.clone());
152
153 let ssl_client = Client::builder()
156 .timeout(Duration::from_secs(5))
157 .build()
158 .unwrap_or_else(|_| Client::new());
159
160 if let Ok(_) = ssl_client.get(&format!("https://{}", domain)).send().await {
161 result.ssl_valid = true;
162 result.ssl_info = Some(SslValidation {
163 ssl_available: true,
164 protocol_version: "TLS".into(), cipher_suite: "Standard".into(),
166 });
167 } else if http_info.https_reachable {
168 result.errors.push("SSL Certificate is invalid or untrusted.".into());
169 }
170
171 result.valid = result.dns_valid && result.http_valid;
172 result
173}
174
175pub async fn validate_domains_bulk(domains: &[String], max_concurrency: usize) -> BulkValidationResult {
176 let start_time = Instant::now();
177 let total = domains.len();
178
179 let mut set = JoinSet::new();
180 let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(max_concurrency));
181
182 for d in domains {
183 let domain = d.clone();
184 let permit = semaphore.clone().acquire_owned().await.unwrap();
185 set.spawn(async move {
186 let res = validate_domain(&domain).await;
187 drop(permit);
188 res
189 });
190 }
191
192 let mut results = Vec::new();
193 let mut valid_count = 0;
194 let mut invalid_count = 0;
195 let mut skipped_count = 0;
196 let mut dns_failed = 0;
197 let mut http_failed = 0;
198 let mut ssl_failed = 0;
199 let mut valid_domains = Vec::new();
200
201 while let Some(res_ok) = set.join_next().await {
202 if let Ok(res) = res_ok {
203 if res.valid {
204 valid_count += 1;
205 valid_domains.push(res.domain.clone());
206 } else if res.skip_reason.is_some() {
207 skipped_count += 1;
208 } else {
209 invalid_count += 1;
210 if !res.dns_valid { dns_failed += 1; }
211 if !res.http_valid { http_failed += 1; }
212 if !res.ssl_valid { ssl_failed += 1; }
213 }
214 results.push(res);
215 }
216 }
217
218 let processing_time_secs = start_time.elapsed().as_secs_f64();
219 let success_rate = if total > 0 { (valid_count as f64 / total as f64) * 100.0 } else { 0.0 };
220 let domains_per_sec = if processing_time_secs > 0.0 { total as f64 / processing_time_secs } else { 0.0 };
221
222 BulkValidationResult {
223 stats: ValidationStats {
224 total,
225 valid: valid_count,
226 invalid: invalid_count,
227 skipped: skipped_count,
228 dns_failed,
229 http_failed,
230 ssl_failed,
231 success_rate,
232 processing_time_secs,
233 domains_per_sec,
234 },
235 valid_domains,
236 results,
237 }
238}