1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct DomainInfoResult {
6 pub domain: String,
7 pub ipv4: Option<String>,
8 pub ipv6: Vec<String>,
9 pub all_ipv4: Vec<String>,
10 pub reverse_dns: Option<String>,
11 pub whois: WhoisInfo,
12 pub ssl: SslInfo,
13 pub dns: DnsInfo,
14 pub open_ports: Vec<String>,
15 pub http_status: Option<String>,
16 pub web_server: Option<String>,
17 pub response_time_ms: Option<f64>,
18 pub security: SecurityInfo,
19 pub security_score: u32,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct WhoisInfo {
24 pub registrar: String,
25 pub creation_date: String,
26 pub expiry_date: String,
27 pub last_updated: String,
28 pub domain_status: Vec<String>,
29 pub registrant: String,
30 pub privacy_protection: String,
31 #[serde(skip_serializing_if = "Vec::is_empty")]
32 pub name_servers: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SslInfo {
37 pub status: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub issued_to: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub issuer: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub protocol_version: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub expiry_date: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub days_until_expiry: Option<i64>,
48 #[serde(skip_serializing_if = "Vec::is_empty")]
49 pub alternative_names: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct DnsInfo {
54 pub nameservers: Vec<String>,
55 pub mx_records: Vec<String>,
56 pub txt_records: Vec<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub spf: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub dmarc: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SecurityInfo {
65 pub https_available: bool,
66 pub https_redirect: bool,
67 pub security_headers: HashMap<String, String>,
68 pub headers_count: usize,
69}
70
71use hickory_resolver::config::*;
72use hickory_resolver::AsyncResolver;
73use reqwest::Client;
74use std::time::{Duration, Instant};
75use tokio::net::TcpStream;
76use tokio::time::timeout;
77
78pub async fn get_domain_info(
79 domain: &str,
80 progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
81) -> Result<DomainInfoResult, Box<dyn std::error::Error + Send + Sync>> {
82 let start_time = Instant::now();
83
84 if let Some(t) = &progress_tx {
85 let _ = t.send(crate::ScanProgress {
86 module: "Domain Info".into(),
87 percentage: 10.0,
88 message: "Starting native mobile domain analysis".into(),
89 status: "Info".into(),
90 }).await;
91 }
92
93 let mut result = DomainInfoResult {
94 domain: domain.to_string(),
95 ipv4: None,
96 ipv6: vec![],
97 all_ipv4: vec![],
98 reverse_dns: None,
99 whois: WhoisInfo {
100 registrar: "Native Fallback".into(),
101 creation_date: "".into(),
102 expiry_date: "".into(),
103 last_updated: "".into(),
104 domain_status: vec![],
105 registrant: "".into(),
106 privacy_protection: "".into(),
107 name_servers: vec![],
108 },
109 ssl: SslInfo {
110 status: "Unknown".into(),
111 issued_to: None,
112 issuer: None,
113 protocol_version: None,
114 expiry_date: None,
115 days_until_expiry: None,
116 alternative_names: vec![],
117 },
118 dns: DnsInfo {
119 nameservers: vec![],
120 mx_records: vec![],
121 txt_records: vec![],
122 spf: None,
123 dmarc: None,
124 },
125 open_ports: vec![],
126 http_status: None,
127 web_server: None,
128 response_time_ms: None,
129 security: SecurityInfo {
130 https_available: false,
131 https_redirect: false,
132 security_headers: HashMap::new(),
133 headers_count: 0,
134 },
135 security_score: 50,
136 };
137
138 let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default());
139
140 if let Ok(response) = resolver.ipv4_lookup(domain).await {
142 for ip in response.iter() {
143 result.all_ipv4.push(ip.to_string());
144 }
145 result.ipv4 = result.all_ipv4.first().cloned();
146 }
147
148 if let Ok(response) = resolver.ipv6_lookup(domain).await {
149 for ip in response.iter() {
150 result.ipv6.push(ip.to_string());
151 }
152 }
153
154 if let Ok(response) = resolver.ns_lookup(domain).await {
155 for ns in response.iter() {
156 result.dns.nameservers.push(ns.to_string());
157 }
158 }
159
160 if let Ok(response) = resolver.mx_lookup(domain).await {
161 for mx in response.iter() {
162 result.dns.mx_records.push(format!("{} {}", mx.preference(), mx.exchange()));
163 }
164 }
165
166 if let Ok(response) = resolver.txt_lookup(domain).await {
167 for txt in response.iter() {
168 let record = txt.to_string();
169 if record.contains("v=spf1") {
170 result.dns.spf = Some(record.clone());
171 }
172 result.dns.txt_records.push(record);
173 }
174 }
175
176 let dmarc_domain = format!("_dmarc.{}", domain);
178 if let Ok(response) = resolver.txt_lookup(dmarc_domain.as_str()).await {
179 for txt in response.iter() {
180 let record = txt.to_string();
181 if record.contains("v=DMARC1") {
182 result.dns.dmarc = Some(record);
183 break;
184 }
185 }
186 }
187
188 if let Some(t) = &progress_tx {
189 let _ = t.send(crate::ScanProgress {
190 module: "Domain Info".into(),
191 percentage: 40.0,
192 message: "DNS Analysis Complete. Checking Ports.".into(),
193 status: "Info".into(),
194 }).await;
195 }
196
197 let target_ports = [80, 443, 21, 22, 25, 3306, 8080, 8443];
199 for port in target_ports {
200 let target = format!("{}:{}", domain, port);
201 if timeout(Duration::from_millis(500), TcpStream::connect(&target)).await.is_ok() {
202 result.open_ports.push(port.to_string());
203 }
204 }
205
206 if let Some(t) = &progress_tx {
207 let _ = t.send(crate::ScanProgress {
208 module: "Domain Info".into(),
209 percentage: 70.0,
210 message: "Checking HTTP/HTTPS footprint in native client".into(),
211 status: "Info".into(),
212 }).await;
213 }
214
215 let client = Client::builder()
217 .timeout(Duration::from_secs(5))
218 .danger_accept_invalid_certs(true)
219 .redirect(reqwest::redirect::Policy::limited(3))
220 .build()
221 .unwrap_or_else(|_| Client::new());
222
223 if let Ok(resp) = client.get(&format!("http://{}", domain)).send().await {
224 result.http_status = Some(resp.status().to_string());
225 if resp.url().scheme() == "https" {
226 result.security.https_redirect = true;
227 }
228 if let Some(srv) = resp.headers().get("server") {
229 result.web_server = Some(srv.to_str().unwrap_or("Unknown").to_string());
230 }
231 }
232
233 if let Ok(resp) = client.get(&format!("https://{}", domain)).send().await {
234 result.security.https_available = true;
235 result.ssl.status = "Valid (Native Check)".into();
236 result.security.headers_count = resp.headers().len();
237
238 for h in ["strict-transport-security", "content-security-policy", "x-frame-options"] {
240 if let Some(val) = resp.headers().get(h) {
241 result.security.security_headers.insert(h.into(), val.to_str().unwrap_or("").into());
242 }
243 }
244 } else {
245 result.ssl.status = "Invalid / Unreachable".into();
246 }
247
248 let mut score = 50;
250 if result.security.https_available { score += 20; }
251 if result.security.https_redirect { score += 10; }
252 score += (result.security.security_headers.len() * 5) as u32;
253 result.security_score = std::cmp::min(100, score);
254 result.response_time_ms = Some(start_time.elapsed().as_millis() as f64);
255
256 if let Some(t) = &progress_tx {
257 let _ = t.send(crate::ScanProgress {
258 module: "Domain Info".into(),
259 percentage: 100.0,
260 message: "Analysis logic loop finished".into(),
261 status: "Info".into(),
262 }).await;
263 }
264
265 Ok(result)
266}