Skip to main content

web_analyzer/
domain_validator_mobile.rs

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    // 1. DNS Check
82    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; // Fast omit
113    }
114
115    // 2. HTTP/HTTPS Check
116    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    // 3. SSL Check Extrapolation (via Reqwest connection success assuming certs are valid)
158    // Detailed parsing requires x509-parser over pure TCP, but basic validation is supported natively here.
159    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(), // Generalized for mobile stub
174            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}