use std::net::IpAddr;
use std::time::{Duration, Instant};
use std::sync::{Mutex, Arc};
use std::sync::mpsc::{channel ,Sender, Receiver};
use futures::{stream, StreamExt};
use tokio::time::{timeout};
use trust_dns_resolver::AsyncResolver;
use trust_dns_resolver::config::*;
use crate::model::Domain;
use crate::result::{ScanStatus, DomainScanResult};
#[cfg(feature = "passive")]
use reqwest::Url;
#[cfg(feature = "passive")]
use crate::model::CertEntry;
#[cfg(feature = "passive")]
use crate::config::{URL_CRT, DEFAULT_USER_AGENT};
#[derive(Clone)]
pub struct DomainScanner {
base_domain: String,
word_list: Vec<String>,
timeout: Duration,
scan_result: DomainScanResult,
tx: Arc<Mutex<Sender<String>>>,
rx: Arc<Mutex<Receiver<String>>>,
passive: bool,
}
impl DomainScanner {
pub fn new() -> Result<DomainScanner, String> {
let (tx, rx) = channel();
let domain_scanner = DomainScanner {
base_domain: String::new(),
word_list: vec![],
timeout: Duration::from_millis(30000),
scan_result: DomainScanResult::new(),
tx: Arc::new(Mutex::new(tx)),
rx: Arc::new(Mutex::new(rx)),
passive: false,
};
Ok(domain_scanner)
}
pub fn set_base_domain(&mut self, base_domain: String) {
self.base_domain = base_domain;
}
pub fn add_word(&mut self, word: String) {
self.word_list.push(word);
}
pub fn set_word_list(&mut self, word_list: Vec<&str>) {
self.word_list.clear();
for word in word_list {
self.word_list.push(word.to_string())
}
}
pub fn set_timeout(&mut self, timeout: Duration){
self.timeout = timeout;
}
pub fn set_passive(&mut self, passive: bool) {
self.passive = passive;
}
async fn scan_domain(&self) -> Result<Vec<Domain>, ()> {
if self.passive {
#[cfg(feature = "passive")]
match timeout(self.timeout, scan_subdomain_passive(self.base_domain.clone(), &self.tx)).await {
Ok(domains) => {
return Ok(domains);
},
Err(_) => {
return Err(());
},
}
#[cfg(not(feature = "passive"))]
return Err(());
}else{
match timeout(self.timeout, scan_subdomain(self.base_domain.clone(), self.word_list.clone(), &self.tx)).await {
Ok(domains) => {
return Ok(domains);
},
Err(_) => {
return Err(());
},
}
}
}
pub async fn run_scan(&mut self){
if self.passive && cfg!(not(feature="passive")) {
self.scan_result.scan_status = ScanStatus::Error;
return;
}
let start_time = Instant::now();
let res = self.scan_domain().await;
match res {
Ok(domains) => {
self.scan_result.domains = domains;
self.scan_result.scan_status = ScanStatus::Done;
},
Err(_) => {
self.scan_result.scan_status = ScanStatus::Timeout;
},
}
self.scan_result.scan_time = Instant::now().duration_since(start_time);
}
pub fn get_result(&mut self) -> DomainScanResult{
return self.scan_result.clone();
}
pub async fn scan(&mut self) -> DomainScanResult {
self.run_scan().await;
self.scan_result.clone()
}
pub fn get_progress_receiver(&self) -> Arc<Mutex<Receiver<String>>> {
self.rx.clone()
}
}
async fn resolve_domain(domain: String) -> Vec<IpAddr> {
let mut ips: Vec<IpAddr> = vec![];
let resolver = AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()).unwrap();
match resolver.lookup_ip(domain).await {
Ok(lip) => {
for ip in lip.iter() {
ips.push(ip);
}
},
Err(_) => {},
}
ips
}
#[cfg(feature = "passive")]
fn extract_domain(target: String) -> String {
let mut domain_name: String = target;
match domain_name.strip_prefix("*.") {
Some(d) => {
domain_name = d.to_string();
},
None => {},
}
domain_name
}
#[cfg(feature = "passive")]
fn is_subdomain(domain: String, apex_domain: String) -> bool {
domain.contains(&apex_domain) && domain.ends_with(&apex_domain) && domain.len() > apex_domain.len()
}
async fn scan_subdomain(base_domain: String, word_list: Vec<String>, ptx: &Arc<Mutex<Sender<String>>>) -> Vec<Domain> {
let mut result: Vec<Domain> = vec![];
let scan_results: Arc<Mutex<Vec<Domain>>> = Arc::new(Mutex::new(vec![]));
let mut target_domains: Vec<String> = vec![];
for word in word_list {
target_domains.push(format!("{}.{}", word, base_domain));
}
let results = stream::iter(target_domains).map(|domain| {
async move {
let ips: Vec<IpAddr> = resolve_domain(domain.clone()).await;
let d = Domain {
domain_name: domain.clone(),
ips: ips,
};
match ptx.lock() {
Ok(lr) => {
match lr.send(domain) {
Ok(_) => {},
Err(_) => {},
}
},
Err(_) => {},
}
d
}
}).buffer_unordered(100);
results.for_each(|domain| async {
if domain.ips.len() > 0 {
scan_results.lock().unwrap().push(domain);
}
}).await;
for domain in scan_results.lock().unwrap().iter() {
result.push(domain.to_owned());
}
result
}
#[cfg(feature = "passive")]
async fn scan_subdomain_passive(base_domain: String, ptx: &Arc<Mutex<Sender<String>>>) -> Vec<Domain> {
let mut result: Vec<Domain> = vec![];
let scan_results: Arc<Mutex<Vec<Domain>>> = Arc::new(Mutex::new(vec![]));
let mut certs: Vec<CertEntry> = vec![];
let url = match Url::parse_with_params(URL_CRT, &[("dNSName", base_domain.clone().as_str()), ("output", "json")]){
Ok(url) => url,
Err(e) => {
println!("{}",e);
return result;
},
};
let client = reqwest::Client::builder().timeout(Duration::from_secs(60)).build().expect("failed to build HTTP reqest client");
let res = client.get(url).header(reqwest::header::USER_AGENT, DEFAULT_USER_AGENT).send().await;
match res {
Ok(r) => {
if r.status().is_success() {
match r.text().await {
Ok(res_text) => {
let certs_json:serde_json::Value = serde_json::from_str(res_text.as_str()).unwrap();
if certs_json.is_array() {
let cert_array = certs_json.as_array().unwrap();
for cert in cert_array {
match serde_json::to_string(cert) {
Ok(cert) => {
let cert:CertEntry = match serde_json::from_str(cert.as_str()){
Ok(cert) => cert,
Err(_) => continue,
};
certs.push(cert);
},
Err(_) => {},
}
}
}
},
Err(_) => {},
};
}
},
Err(_) => {},
}
let mut target_domains: Vec<String> = vec![];
for cert in certs {
let domain_name: String = extract_domain(cert.common_name);
if is_subdomain(domain_name.clone(), base_domain.clone()) && !target_domains.contains(&domain_name) {
target_domains.push(domain_name);
}
let name_values: Vec<&str> = cert.name_value.trim().split("\n").collect();
for value in name_values {
let name: String = extract_domain(value.to_string());
if is_subdomain(name.clone(), base_domain.clone()) && !target_domains.contains(&name) {
target_domains.push(name);
}
}
}
let results = stream::iter(target_domains).map(|domain| {
async move {
let ips: Vec<IpAddr> = resolve_domain(domain.clone()).await;
let d = Domain {
domain_name: domain.clone(),
ips: ips,
};
match ptx.lock() {
Ok(lr) => {
match lr.send(domain) {
Ok(_) => {},
Err(_) => {},
}
},
Err(_) => {},
}
d
}
}).buffer_unordered(100);
results.for_each(|domain| async {
if domain.ips.len() > 0 {
scan_results.lock().unwrap().push(domain);
}
}).await;
for domain in scan_results.lock().unwrap().iter() {
result.push(domain.to_owned());
}
result
}