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
110 .errors
111 .push("DNS Resolution failed. No A/AAAA records.".into());
112 return result; }
114
115 let start_http = Instant::now();
117 let client = crate::http_client_builder()
118 .timeout(Duration::from_secs(5))
119 .danger_accept_invalid_certs(true)
120 .redirect(reqwest::redirect::Policy::limited(3))
121 .build()
122 .unwrap_or_else(|_| Client::new());
123
124 let mut http_info = HttpValidation {
125 http_reachable: false,
126 https_reachable: false,
127 http_status: None,
128 https_status: None,
129 redirects_to_https: false,
130 response_time_ms: 0,
131 };
132
133 if let Ok(resp) = client.get(format!("http://{}", domain)).send().await {
134 http_info.http_reachable = true;
135 http_info.http_status = Some(resp.status().as_u16());
136 if resp.url().scheme() == "https" {
137 http_info.redirects_to_https = true;
138 }
139 }
140
141 if let Ok(resp) = client.get(format!("https://{}", domain)).send().await {
142 http_info.https_reachable = true;
143 http_info.https_status = Some(resp.status().as_u16());
144 }
145
146 http_info.response_time_ms = start_http.elapsed().as_millis();
147 result.http_valid = http_info.http_reachable || http_info.https_reachable;
148
149 if !result.http_valid {
150 result
151 .errors
152 .push("Host is unreachable via HTTP/HTTPS.".into());
153 }
154
155 result.http_info = Some(http_info.clone());
156
157 let ssl_client = crate::http_client_builder()
160 .timeout(Duration::from_secs(5))
161 .build()
162 .unwrap_or_else(|_| Client::new());
163
164 if ssl_client
165 .get(format!("https://{}", domain))
166 .send()
167 .await
168 .is_ok()
169 {
170 result.ssl_valid = true;
171 result.ssl_info = Some(SslValidation {
172 ssl_available: true,
173 protocol_version: "TLS".into(), cipher_suite: "Standard".into(),
175 });
176 } else if http_info.https_reachable {
177 result
178 .errors
179 .push("SSL Certificate is invalid or untrusted.".into());
180 }
181
182 result.valid = result.dns_valid && result.http_valid;
183 result
184}
185
186pub async fn validate_domains_bulk(
187 domains: &[String],
188 max_concurrency: usize,
189) -> BulkValidationResult {
190 let start_time = Instant::now();
191 let total = domains.len();
192
193 let mut set = JoinSet::new();
194 let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(max_concurrency));
195
196 for d in domains {
197 let domain = d.clone();
198 let permit = semaphore.clone().acquire_owned().await.unwrap();
199 set.spawn(async move {
200 let res = validate_domain(&domain).await;
201 drop(permit);
202 res
203 });
204 }
205
206 let mut results = Vec::new();
207 let mut valid_count = 0;
208 let mut invalid_count = 0;
209 let mut skipped_count = 0;
210 let mut dns_failed = 0;
211 let mut http_failed = 0;
212 let mut ssl_failed = 0;
213 let mut valid_domains = Vec::new();
214
215 while let Some(res_ok) = set.join_next().await {
216 if let Ok(res) = res_ok {
217 if res.valid {
218 valid_count += 1;
219 valid_domains.push(res.domain.clone());
220 } else if res.skip_reason.is_some() {
221 skipped_count += 1;
222 } else {
223 invalid_count += 1;
224 if !res.dns_valid {
225 dns_failed += 1;
226 }
227 if !res.http_valid {
228 http_failed += 1;
229 }
230 if !res.ssl_valid {
231 ssl_failed += 1;
232 }
233 }
234 results.push(res);
235 }
236 }
237
238 let processing_time_secs = start_time.elapsed().as_secs_f64();
239 let success_rate = if total > 0 {
240 (valid_count as f64 / total as f64) * 100.0
241 } else {
242 0.0
243 };
244 let domains_per_sec = if processing_time_secs > 0.0 {
245 total as f64 / processing_time_secs
246 } else {
247 0.0
248 };
249
250 BulkValidationResult {
251 stats: ValidationStats {
252 total,
253 valid: valid_count,
254 invalid: invalid_count,
255 skipped: skipped_count,
256 dns_failed,
257 http_failed,
258 ssl_failed,
259 success_rate,
260 processing_time_secs,
261 domains_per_sec,
262 },
263 valid_domains,
264 results,
265 }
266}