use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::api::{check_dns, DnsCheckRequest};
pub async fn check_common_subdomains(req: &SubdomainCheckRequest) -> Result<SubdomainCheckResult> {
const MAX_TOTAL_SUBDOMAINS: usize = 100;
let mut subdomains = vec![
"www", "mail", "ftp", "api", "cdn", "staging", "dev", "admin",
"vpn", "test", "beta", "app", "mobile", "auth", "secure",
];
if let Some(extra) = &req.extra_subdomains {
let remaining_capacity = MAX_TOTAL_SUBDOMAINS.saturating_sub(subdomains.len());
let to_add = std::cmp::min(extra.len(), remaining_capacity);
for extra_sub in extra.iter().take(to_add) {
subdomains.push(extra_sub.as_str());
}
}
let timeout = req.timeout_secs;
let tasks: Vec<_> = subdomains
.iter()
.map(|sub| {
let sub = sub.to_string();
let domain = req.domain.clone();
async move {
let full_domain = format!("{}.{}", sub, domain);
check_subdomain(&full_domain, timeout).await
}
})
.collect();
let handles: Vec<_> = tasks.into_iter().map(tokio::spawn).collect();
let mut results = Vec::with_capacity(handles.len());
for h in handles { if let Ok(v) = h.await { results.push(v); } }
Ok(SubdomainCheckResult {
domain: req.domain.clone(),
subdomains: results,
})
}
async fn check_subdomain(subdomain: &str, timeout_secs: u64) -> SubdomainStatus {
let dns_req = DnsCheckRequest {
domain: subdomain.to_string(),
record_types: vec!["A".to_string()],
timeout_secs,
..Default::default()
};
let dns_resolves = match check_dns(&dns_req).await {
Ok(results) => !results.is_empty() && !results[0].answers.is_empty(),
Err(_) => false,
};
let http_status = if dns_resolves {
match check_http_status(subdomain, timeout_secs).await {
Some(status) => Some(status),
None => None,
}
} else {
None
};
let tls_valid = if dns_resolves && http_status.is_some() {
if let Some(status) = &http_status {
if status >= &200 && status < &400 {
match crate::api::check_tls_chain(&crate::api::TlsCheckRequest {
hostname: subdomain.to_string(),
port: 443,
check_dane: false,
timeout_secs,
})
.await
{
Ok(result) => Some(result.valid),
Err(_) => None,
}
} else {
None
}
} else {
None
}
} else {
None
};
SubdomainStatus {
subdomain: subdomain.to_string(),
dns_resolves,
http_status,
tls_valid,
}
}
async fn check_http_status(hostname: &str, timeout_secs: u64) -> Option<u16> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::none())
.build()
.ok()?;
match client
.get(format!("https://{}", hostname))
.send()
.await
{
Ok(response) => Some(response.status().as_u16()),
Err(_) => {
match client
.get(format!("http://{}", hostname))
.send()
.await
{
Ok(response) => Some(response.status().as_u16()),
Err(_) => None,
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubdomainCheckRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub extra_subdomains: Option<Vec<String>>,
}
fn default_timeout() -> u64 { 10 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubdomainCheckResult {
pub domain: String,
pub subdomains: Vec<SubdomainStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubdomainStatus {
pub subdomain: String,
pub dns_resolves: bool,
#[serde(default)]
pub http_status: Option<u16>,
#[serde(default)]
pub tls_valid: Option<bool>,
}