use clap::Parser;
use console::Style;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
use tokio::time::sleep;
#[derive(Parser, Debug, Clone)]
#[command(name = "domaincheck", version)]
#[command(about = "Check domainname availability on CLI using RDAP (with WHOIS fallback)", long_about = None)]
#[command(help_template = "{before-help}{name} {version}\n{about}\n{usage-heading}\n {usage}\n{all-args}{after-help}")]
pub struct Args {
#[arg(value_parser = validate_domain)]
pub domain: Option<String>,
#[arg(short, long, num_args = 1.., value_delimiter = ' ')]
pub tld: Option<Vec<String>>,
#[arg(short, long)]
pub file: Option<String>,
#[arg(short, long)]
pub json: bool,
#[arg(short, long, hide = true, default_value_t = true)]
pub pretty: bool,
#[arg(short, long, hide = true, default_value_t = true)]
pub info: bool,
#[arg(short, long, hide = true, default_value_t = true)]
pub bootstrap: bool,
#[arg(short, long)]
pub ui: bool,
#[arg(short, long, default_value = "10")]
pub concurrency: usize,
#[arg(short, long)]
pub no_whois: bool,
#[arg(short, long)]
pub verbose: bool,
#[arg(short, long)]
pub debug: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct DomainStatus {
domain: String,
available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
info: Option<DomainInfo>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct DomainInfo {
registrar: Option<String>,
creation_date: Option<String>,
expiration_date: Option<String>,
status: Vec<String>,
}
fn validate_domain(domain: &str) -> Result<String, String> {
let domain = domain.trim();
if domain.is_empty() {
return Err("Domainname cannot be empty".into());
}
let re = regex::Regex::new(r"^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$").unwrap();
if !re.is_match(domain) {
return Err("Invalid domain format. Use something like 'example' or 'example.com'.".into());
}
Ok(domain.to_string())
}
fn extract_parts(domain: &str) -> (String, Option<String>) {
let parts: Vec<&str> = domain.split('.').collect();
if parts.len() >= 2 {
let base_name = parts[0].to_string();
let tld = parts[1..].join(".");
(base_name, Some(tld))
} else {
(domain.to_string(), None)
}
}
fn normalize_domains(base: &str, cli_tlds: &Option<Vec<String>>, extracted_tld: Option<String>) -> Vec<String> {
match cli_tlds {
Some(tlds) => tlds.iter().map(|tld| format!("{}.{}", base, tld)).collect(),
None => {
let tld = extracted_tld.unwrap_or_else(|| "com".to_string());
vec![format!("{}.{}", base, tld)]
}
}
}
fn rdap_registry_map() -> HashMap<&'static str, &'static str> {
HashMap::from([
("com", "https://rdap.verisign.com/com/v1/domain/"),
("net", "https://rdap.verisign.com/net/v1/domain/"),
("org", "https://rdap.pir.org/domain/"),
("info", "https://rdap.afilias.info/rdap/info/domain/"),
("biz", "https://rdap.nic.biz/domain/"),
("app", "https://rdap.nic.google/domain/"),
("dev", "https://rdap.nic.google/domain/"),
("page", "https://rdap.nic.google/domain/"),
("blog", "https://rdap.nic.blog/domain/"),
("shop", "https://rdap.nic.shop/domain/"),
("xyz", "https://rdap.nic.xyz/domain/"),
("tech", "https://rdap.nic.tech/domain/"),
("ai", "https://rdap.nic.ai/domain/"),
("au", "https://rdap.auda.org.au/domain/"),
("cc", "https://rdap.verisign.com/cc/v1/domain/"),
("ch", "https://rdap.nic.ch/domain/"),
("co", "https://rdap.nic.co/domain/"),
("ca", "https://rdap.cira.ca/domain/"),
("de", "https://rdap.denic.de/domain/"),
("es", "https://rdap.nic.es/domain/"),
("eu", "https://rdap.eu.org/domain/"),
("fr", "https://rdap.nic.fr/domain/"),
("io", "https://rdap.nic.io/domain/"),
("in", "https://rdap.registry.in/domain/"),
("it", "https://rdap.nic.it/domain/"),
("jp", "https://rdap.jprs.jp/domain/"),
("me", "https://rdap.nic.me/domain/"),
("nl", "https://rdap.domain-registry.nl/domain/"),
("uk", "https://rdap.nominet.uk/domain/"),
("us", "https://rdap.nic.us/domain/"),
("tv", "https://rdap.verisign.com/tv/v1/domain/"),
("zone", "https://rdap.nic.zone/domain/"),
])
}
struct BootstrapCache {
endpoints: HashMap<String, String>,
last_update: Instant,
}
impl BootstrapCache {
fn new() -> Self {
Self { endpoints: HashMap::new(), last_update: Instant::now() }
}
fn get(&self, tld: &str) -> Option<String> {
self.endpoints.get(tld).cloned()
}
fn insert(&mut self, tld: String, endpoint: String) {
self.endpoints.insert(tld, endpoint);
self.last_update = Instant::now();
}
fn is_stale(&self) -> bool {
self.last_update.elapsed() > Duration::from_secs(3600)
}
}
lazy_static::lazy_static! {
static ref BOOTSTRAP_CACHE: Mutex<BootstrapCache> = Mutex::new(BootstrapCache::new());
}
async fn find_endpoint_for_tld(tld: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
{
let cache = BOOTSTRAP_CACHE.lock().unwrap();
if !cache.is_stale() {
if let Some(endpoint) = cache.get(tld) {
return Ok(endpoint);
}
}
}
let bootstrap_url = "https://data.iana.org/rdap/dns.json";
let client = reqwest::Client::builder().timeout(Duration::from_secs(5)).build()?;
let response = client.get(bootstrap_url).send().await?;
if response.status().is_success() {
let json: serde_json::Value = response.json().await?;
if let Some(services) = json.get("services").and_then(|s| s.as_array()) {
for service in services {
if let Some(service_array) = service.as_array() {
if service_array.len() >= 2 {
if let Some(tlds) = service_array[0].as_array() {
for t in tlds {
if let Some(t_str) = t.as_str() {
if t_str.to_lowercase() == tld.to_lowercase() {
if let Some(urls) = service_array[1].as_array() {
if let Some(url) = urls.first().and_then(|u| u.as_str()) {
let endpoint = format!("{}/domain/", url.trim_end_matches('/'));
let mut cache = BOOTSTRAP_CACHE.lock().unwrap();
cache.insert(tld.to_string(), endpoint.clone());
return Ok(endpoint);
}
}
}
}
}
}
}
}
}
}
}
Err("No RDAP endpoint found for this TLD".into())
}
async fn check_rdap(domain: &str, endpoint_base: &str) -> Result<(bool, Option<serde_json::Value>), reqwest::Error> {
let url = format!("{}{}", endpoint_base, domain);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5)) .build()?;
let res = client.get(&url).send().await?;
match res.status() {
StatusCode::OK => {
let json = res.json::<serde_json::Value>().await?;
Ok((false, Some(json)))
}
StatusCode::NOT_FOUND => Ok((true, None)),
StatusCode::TOO_MANY_REQUESTS => {
sleep(Duration::from_millis(500)).await;
let retry_res = client.get(&url).send().await?;
match retry_res.status() {
StatusCode::OK => {
let json = retry_res.json::<serde_json::Value>().await?;
Ok((false, Some(json)))
}
StatusCode::NOT_FOUND => Ok((true, None)),
_ => Ok((false, None)),
}
}
_ => Ok((false, None)),
}
}
async fn check_whois(domain: &str) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let output = tokio::process::Command::new("whois").arg(domain).output().await?;
let output_text = String::from_utf8_lossy(&output.stdout).to_lowercase();
let available_patterns = [
"no match",
"not found",
"no data found",
"no entries found",
"domain not found",
"domain available",
"status: available",
"status: free",
"no information available",
"not registered",
];
let error_patterns = ["rate limit exceeded", "too many requests", "try again later", "quota exceeded"];
for pattern in &error_patterns {
if output_text.contains(pattern) {
sleep(Duration::from_millis(1000)).await;
let retry_output = tokio::process::Command::new("whois").arg(domain).output().await?;
let retry_text = String::from_utf8_lossy(&retry_output.stdout).to_lowercase();
for available_pattern in available_patterns {
if retry_text.contains(available_pattern) {
return Ok(true);
}
}
return Ok(false);
}
}
for pattern in available_patterns {
if output_text.contains(pattern) {
return Ok(true);
}
}
Ok(false)
}
fn extract_domain_info(json: &serde_json::Value) -> Option<DomainInfo> {
let mut info = DomainInfo { registrar: None, creation_date: None, expiration_date: None, status: Vec::new() };
if let Some(entities) = json.get("entities").and_then(|e| e.as_array()) {
for entity in entities {
if let Some(roles) = entity.get("roles").and_then(|r| r.as_array()) {
let is_registrar = roles.iter().any(|role| role.as_str() == Some("registrar"));
if is_registrar {
if let Some(name) =
entity.get("vcardArray").and_then(|v| v.as_array()).and_then(|a| a.get(1)).and_then(|a| a.as_array()).and_then(|items| {
for item in items {
if let Some(item_array) = item.as_array() {
if item_array.len() >= 4 {
if let Some(first) = item_array.first().and_then(|f| f.as_str()) {
if first == "fn" {
return item_array.get(3).and_then(|n| n.as_str()).map(String::from);
}
}
}
}
}
None
}) {
info.registrar = Some(name);
break;
}
else if let Some(public_ids) = entity.get("publicIds").and_then(|p| p.as_array()) {
if let Some(id) = public_ids.first().and_then(|id| id.get("identifier")).and_then(|i| i.as_str()) {
info.registrar = Some(id.to_string());
break;
}
}
else if let Some(handle) = entity.get("handle").and_then(|h| h.as_str()) {
info.registrar = Some(handle.to_string());
break;
}
else if let Some(name) = entity.get("name").and_then(|n| n.as_str()) {
info.registrar = Some(name.to_string());
break;
}
}
}
}
}
if let Some(events) = json.get("events").and_then(|e| e.as_array()) {
for event in events {
if let (Some(event_action), Some(event_date)) = (event.get("eventAction").and_then(|a| a.as_str()), event.get("eventDate").and_then(|d| d.as_str()))
{
match event_action {
"registration" => info.creation_date = Some(event_date.to_string()),
"expiration" => info.expiration_date = Some(event_date.to_string()),
_ => {}
}
}
}
}
if let Some(statuses) = json.get("status").and_then(|s| s.as_array()) {
for status in statuses {
if let Some(status_str) = status.as_str() {
info.status.push(status_str.to_string());
}
}
}
Some(info)
}
fn format_domain_info(info: &DomainInfo) -> String {
let mut parts = Vec::new();
if let Some(creation) = &info.creation_date {
parts.push(format!(" {}.", creation));
}
if let Some(expiration) = &info.expiration_date {
parts.push(format!(".{}", expiration));
}
if let Some(registrar) = &info.registrar {
parts.push(format!(" @\"{}\"", registrar));
}
if !info.status.is_empty() {
parts.push(format!(" {}", info.status.join(", ")));
}
parts.join("")
}
fn display_interactive_dashboard(domains: &[DomainStatus]) -> Result<(), Box<dyn std::error::Error>> {
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Paragraph, Row, Table},
};
let (tx, rx) = std::sync::mpsc::channel();
ctrlc::set_handler(move || {
tx.send(()).expect("Could not send signal on channel");
})
.expect("Error setting Ctrl-C handler");
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut selected_index = 0;
loop {
if rx.try_recv().is_ok() {
break;
}
terminal.draw(|f| {
let size = f.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3), Constraint::Min(10), Constraint::Length(5), ]
.as_ref(),
)
.split(size);
let title = Paragraph::new("Domain Checker Dashboard")
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
.block(Block::default().borders(Borders::ALL));
f.render_widget(title, chunks[0]);
let header_cells = ["Domain", "Status", "Registrar", "Created", "Expires"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Yellow)));
let header = Row::new(header_cells).height(1).bottom_margin(1);
let rows = domains.iter().enumerate().map(|(i, domain)| {
let status_style = match domain.available {
Some(true) => Style::default().fg(Color::Green),
Some(false) => Style::default().fg(Color::Red),
None => Style::default().fg(Color::Gray),
};
let status_text = match domain.available {
Some(true) => "Available",
Some(false) => "Taken",
None => "Unknown",
};
let default_info = DomainInfo { registrar: None, creation_date: None, expiration_date: None, status: Vec::new() };
let info = domain.info.as_ref().unwrap_or(&default_info);
let style = if i == selected_index { Style::default().add_modifier(Modifier::REVERSED) } else { Style::default() };
let registrar_text = info.registrar.clone().unwrap_or_else(|| "-".to_string());
let creation_text = info.creation_date.clone().unwrap_or_else(|| "-".to_string());
let expiration_text = info.expiration_date.clone().unwrap_or_else(|| "-".to_string());
let cells = [
Cell::from(domain.domain.clone()),
Cell::from(status_text).style(status_style),
Cell::from(registrar_text),
Cell::from(creation_text),
Cell::from(expiration_text),
];
Row::new(cells).style(style).height(1)
});
let domain_table = Table::new(rows, [1]).header(header).block(Block::default().title("Domain Status").borders(Borders::ALL)).widths([
Constraint::Percentage(30),
Constraint::Percentage(15),
Constraint::Percentage(20),
Constraint::Percentage(17),
Constraint::Percentage(18),
]);
f.render_widget(domain_table, chunks[1]);
let help_text = "â/â: Navigate | Enter: View Details | s: Suggest Alternatives | q / Ctrl_c: Quit";
let help = Paragraph::new(help_text).style(Style::default().fg(Color::White)).block(Block::default().borders(Borders::ALL));
f.render_widget(help, chunks[2]);
})?;
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
KeyCode::Up => {
selected_index = selected_index.saturating_sub(1);
}
KeyCode::Down => {
if selected_index < domains.len() - 1 {
selected_index += 1;
}
}
KeyCode::Enter => {
}
KeyCode::Char('s') => {
if selected_index < domains.len() {
if let Some(false) = domains[selected_index].available {
}
}
}
_ => {}
}
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
Ok(())
}
async fn check_domains_in_parallel(domains: Vec<String>, args: Args, registry_map: HashMap<&'static str, &'static str>) -> Vec<DomainStatus> {
let max_concurrent = args.concurrency.min(100); let semaphore = Arc::new(Semaphore::new(max_concurrent));
let results = Arc::new(Mutex::new(Vec::new()));
let endpoint_last_used = Arc::new(Mutex::new(HashMap::<String, Instant>::new()));
let registry_map = Arc::new(registry_map);
let mut handles = Vec::new();
for domain in domains {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let registry_map = registry_map.clone(); let results = Arc::clone(&results);
let endpoint_last_used = Arc::clone(&endpoint_last_used);
let args = args.clone();
let handle = tokio::spawn(async move {
let status = tokio::time::timeout(
Duration::from_secs(10), check_single_domain(&domain, &args, ®istry_map, endpoint_last_used, false),
)
.await
.unwrap_or_else(|_| {
if args.debug || args.verbose {
println!("â ī¸ RDAP timeout for {}, trying WHOIS instead", domain);
}
let fallback_result = tokio::runtime::Handle::current().block_on(check_whois(&domain));
let available = fallback_result.unwrap_or(false);
let status = DomainStatus { domain: domain.clone(), available: Some(available), info: None };
if !args.json && !args.ui {
let green = Style::new().green().bold();
let red = Style::new().red().bold();
let gray = Style::new().dim();
if available && !args.json {
if args.info {
println!("{} {} AVAILABLE {}", green.apply_to("đĸ"), domain, gray.apply_to("(No info available for unregistered domains)"));
} else {
println!("{} {} AVAILABLE", green.apply_to("đĸ"), domain);
}
} else if args.info {
println!("{} {} TAKEN {}", red.apply_to("đ´"), domain, gray.apply_to("(No info available via WHOIS fallback)"));
} else {
println!("{} {} TAKEN", red.apply_to("đ´"), domain);
}
}
status
});
let mut results_lock = results.lock().unwrap();
results_lock.push(status);
drop(permit);
});
handles.push(handle);
}
for handle in handles {
let _ = handle.await;
}
let results_lock = results.lock().unwrap();
results_lock.clone()
}
async fn check_single_domain(
domain: &str, args: &Args, registry_map: &Arc<HashMap<&'static str, &'static str>>, endpoint_last_used: Arc<Mutex<HashMap<String, Instant>>>,
is_bulk_mode: bool,
) -> DomainStatus {
let parts: Vec<&str> = domain.split('.').collect();
let tld = if parts.len() >= 2 { parts.last().unwrap().to_string() } else { "".to_string() };
let mut domain_status = DomainStatus { domain: domain.to_string(), available: None, info: None };
let green = Style::new().green().bold();
let red = Style::new().red().bold();
let blue = Style::new().blue();
let gray = Style::new().dim();
let mut rdap_successful = false;
if let Some(endpoint_base) = registry_map.get(tld.as_str()) {
let endpoint = endpoint_base.to_string();
let should_delay = {
let last_used_map = endpoint_last_used.lock().unwrap();
if let Some(last_time) = last_used_map.get(&endpoint) {
let elapsed = last_time.elapsed();
if elapsed < Duration::from_millis(100) {
Some(Duration::from_millis(100) - elapsed)
} else {
None
}
} else {
None
}
};
if let Some(delay) = should_delay {
sleep(delay).await;
}
{
let mut last_used_map = endpoint_last_used.lock().unwrap();
last_used_map.insert(endpoint.clone(), Instant::now());
}
match tokio::time::timeout(
Duration::from_secs(3), check_rdap(domain, endpoint_base),
)
.await
{
Ok(Ok((true, _))) => {
domain_status.available = Some(true);
rdap_successful = true;
if !args.json && !is_bulk_mode {
print_domain_available(domain, false, args, &green, &gray);
}
}
Ok(Ok((false, Some(json)))) => {
domain_status.available = Some(false);
domain_status.info = extract_domain_info(&json);
rdap_successful = true;
if !args.json && !is_bulk_mode {
print_domain_taken(domain, false, args, &domain_status.info, &red, &blue, &gray);
}
}
Ok(Ok((false, None))) => {
domain_status.available = Some(false);
rdap_successful = true;
if !args.json && !is_bulk_mode {
print_domain_taken(domain, false, args, &None, &red, &blue, &gray);
}
}
_ => {
if args.debug || args.verbose {
println!("â ī¸ RDAP lookup failed for {}", domain);
}
}
}
} else if args.bootstrap && !rdap_successful {
if args.debug || args.verbose {
println!("đ No known RDAP endpoint for .{}, trying bootstrap registry...", tld);
}
match tokio::time::timeout(Duration::from_secs(2), find_endpoint_for_tld(&tld)).await {
Ok(Ok(endpoint_base)) => match tokio::time::timeout(Duration::from_secs(3), check_rdap(domain, &endpoint_base)).await {
Ok(Ok((true, _))) => {
domain_status.available = Some(true);
rdap_successful = true;
if !args.json && !is_bulk_mode {
print_domain_available(domain, false, args, &green, &gray);
}
}
Ok(Ok((false, Some(json)))) => {
domain_status.available = Some(false);
domain_status.info = extract_domain_info(&json);
rdap_successful = true;
if !args.json && !is_bulk_mode {
print_domain_taken(domain, false, args, &domain_status.info, &red, &blue, &gray);
}
}
Ok(Ok((false, None))) => {
domain_status.available = Some(false);
rdap_successful = true;
if !args.json && !is_bulk_mode {
print_domain_taken(domain, false, args, &None, &red, &blue, &gray);
}
}
_ => {
if args.debug || args.verbose {
println!("â ī¸ Bootstrap RDAP lookup failed for {}", domain);
}
}
},
_ => {
if args.debug || args.verbose {
println!("â ī¸ Failed to find RDAP endpoint for .{}", tld);
}
}
}
}
if !rdap_successful && !args.no_whois {
if args.debug || args.verbose {
println!("đ Trying WHOIS fallback for {}...", domain);
}
match tokio::time::timeout(Duration::from_secs(5), check_whois(domain)).await {
Ok(Ok(available)) => {
domain_status.available = Some(available);
if !args.json && !is_bulk_mode {
if available {
print_domain_available(domain, true, args, &green, &gray);
} else {
print_domain_taken(domain, true, args, &None, &red, &blue, &gray);
}
}
}
_ => {
if args.debug || args.verbose {
println!("â ī¸ WHOIS lookup failed for {}", domain);
}
}
}
}
if domain_status.available.is_none() && (args.debug || args.verbose) {
println!("â ī¸ Could not determine availability for {}", domain);
}
domain_status
}
fn validate_domain_line(line: &str, line_num: usize) -> Result<String, String> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return Err(format!("Line {} is empty or a comment - skipping", line_num));
}
let domain_part = line.split('#').next().unwrap_or("").trim();
if domain_part.is_empty() {
return Err(format!("Line {} contains only a comment - skipping", line_num));
}
validate_domain(domain_part)
}
fn read_domains_from_file(
file_path: &str,
tlds: &Option<Vec<String>>,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let path = Path::new(file_path);
if !path.exists() {
return Err(format!("File not found: {}", file_path).into());
}
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut domains = Vec::new();
let mut invalid_lines = Vec::new();
let mut line_num = 0;
for line in reader.lines() {
line_num += 1;
match line {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
match validate_domain_line(trimmed, line_num) {
Ok(domain) => {
let parts: Vec<&str> = domain.split('.').collect();
if parts.len() >= 2 {
domains.push(domain); } else if let Some(tld_list) = tlds {
for tld in tld_list {
domains.push(format!("{}.{}", domain, tld));
}
} else {
domains.push(format!("{}.com", domain));
}
}
Err(e) => {
invalid_lines.push(format!("Line {}: {} - {}", line_num, trimmed, e));
}
}
}
Err(e) => {
invalid_lines.push(format!("Line {}: Error reading line - {}", line_num, e));
}
}
}
if !invalid_lines.is_empty() {
println!("â ī¸ Found {} invalid entries in the file:", invalid_lines.len());
for invalid in &invalid_lines[..invalid_lines.len().min(10)] {
println!(" {}", invalid);
}
if invalid_lines.len() > 10 {
println!(" ... and {} more invalid entries", invalid_lines.len() - 10);
}
println!();
}
if domains.is_empty() {
return Err("No valid domains found in the file.".into());
}
Ok(domains)
}
async fn check_domains_in_bulk(domains: Vec<String>, args: Args, registry_map: HashMap<&'static str, &'static str>) -> Vec<DomainStatus> {
let max_concurrent = args.concurrency.min(100);
let semaphore = Arc::new(Semaphore::new(max_concurrent));
let results = Arc::new(Mutex::new(Vec::new()));
let endpoint_last_used = Arc::new(Mutex::new(HashMap::<String, Instant>::new()));
let available_count = Arc::new(AtomicUsize::new(0));
let taken_count = Arc::new(AtomicUsize::new(0));
let unknown_count = Arc::new(AtomicUsize::new(0));
if !args.json && !args.ui && args.pretty {
println!("Starting bulk domain check with concurrency: {}", max_concurrent);
println!("Results will stream as they complete:\n");
}
let mut domains_by_tld: HashMap<String, Vec<String>> = HashMap::new();
for domain in domains {
let parts: Vec<&str> = domain.split('.').collect();
let tld = if parts.len() >= 2 { parts.last().unwrap().to_string() } else { "unknown".to_string() };
domains_by_tld.entry(tld).or_default().push(domain);
}
let registry_map = Arc::new(registry_map);
let mut handles = Vec::new();
for (tld, domain_group) in domains_by_tld {
let needs_rate_limiting = registry_map.contains_key(tld.as_str());
for domain in domain_group {
if needs_rate_limiting {
sleep(Duration::from_millis(10)).await;
}
let permit = semaphore.clone().acquire_owned().await.unwrap();
let registry_map = registry_map.clone();
let results = Arc::clone(&results);
let endpoint_last_used = Arc::clone(&endpoint_last_used);
let available_count = Arc::clone(&available_count);
let taken_count = Arc::clone(&taken_count);
let unknown_count = Arc::clone(&unknown_count);
let args = args.clone();
let handle = tokio::spawn(async move {
let status = tokio::time::timeout(
Duration::from_secs(8), check_single_domain(&domain, &args, ®istry_map, endpoint_last_used, true),
)
.await
.unwrap_or_else(|_| {
if args.debug || args.verbose {
println!("â ī¸ Timeout for {}, trying WHOIS", domain);
}
let fallback_result = tokio::runtime::Handle::current().block_on(check_whois(&domain));
let available = fallback_result.unwrap_or(false);
DomainStatus { domain: domain.clone(), available: Some(available), info: None }
});
match status.available {
Some(true) => available_count.fetch_add(1, Ordering::Relaxed),
Some(false) => taken_count.fetch_add(1, Ordering::Relaxed),
None => unknown_count.fetch_add(1, Ordering::Relaxed),
};
if !args.json && !args.ui {
let green_local = Style::new().green().bold();
let red_local = Style::new().red().bold();
let blue_local = Style::new().blue();
let gray_local = Style::new().dim();
match status.available {
Some(true) => {
if args.info {
println!(
"{} {} AVAILABLE {}",
green_local.apply_to("đĸ"),
domain,
gray_local.apply_to("(No info available for unregistered domains)")
);
} else {
println!("{} {} AVAILABLE", green_local.apply_to("đĸ"), domain);
}
}
Some(false) => {
if args.info {
if let Some(domain_info) = &status.info {
println!("{} {} TAKEN {}", red_local.apply_to("đ´"), domain, blue_local.apply_to(format_domain_info(domain_info)));
} else {
println!("{} {} TAKEN {}", red_local.apply_to("đ´"), domain, gray_local.apply_to("(No info available)"));
}
} else {
println!("{} {} TAKEN", red_local.apply_to("đ´"), domain);
}
}
None => println!("â ī¸ {} status unknown", domain),
}
}
let mut results_lock = results.lock().unwrap();
results_lock.push(status);
drop(permit);
});
handles.push(handle);
}
}
for handle in handles {
let _ = handle.await;
}
let final_available = available_count.load(Ordering::Relaxed);
let final_taken = taken_count.load(Ordering::Relaxed);
let final_unknown = unknown_count.load(Ordering::Relaxed);
let results_lock = results.lock().unwrap();
let result_set = results_lock.clone();
if !args.json && args.file.is_some() {
println!(
"\nâ
{} domains processed: đĸ {} available, đ´ {} taken, â ī¸ {} unknown",
result_set.len(),
final_available,
final_taken,
final_unknown
);
}
result_set
}
fn print_domain_available(domain: &str, via_whois: bool, args: &Args, green: &Style, gray: &Style) {
let suffix = if via_whois && args.debug { " (via WHOIS)" } else { "" };
if args.info {
println!(
"{} {}{} AVAILABLE {}",
green.apply_to("đĸ"),
domain,
suffix,
gray.apply_to("(No info available for unregistered domains)")
);
} else {
println!("{} {}{} AVAILABLE", green.apply_to("đĸ"), domain, suffix);
}
}
fn print_domain_taken(domain: &str, via_whois: bool, args: &Args, info: &Option<DomainInfo>, red: &Style, blue: &Style, gray: &Style) {
let suffix = if via_whois && args.debug { " (via WHOIS)" } else { "" };
if args.info {
if via_whois {
println!(
"{} {}{} TAKEN {}",
red.apply_to("đ´"),
domain,
suffix,
gray.apply_to("(Detailed info not available via WHOIS fallback)")
);
} else if let Some(domain_info) = info {
println!("{} {}{} TAKEN {}", red.apply_to("đ´"), domain, suffix, blue.apply_to(format_domain_info(domain_info)));
} else {
println!("{} {}{} TAKEN {}", red.apply_to("đ´"), domain, suffix, gray.apply_to("(No info available)"));
}
} else {
println!("{} {}{} TAKEN", red.apply_to("đ´"), domain, suffix);
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let registry_map = rdap_registry_map();
let domains = if let Some(file_path) = &args.file {
read_domains_from_file(file_path, &args.tld)?
} else if let Some(domain_name) = &args.domain {
let (base_name, tld_from_domain) = extract_parts(domain_name);
normalize_domains(&base_name, &args.tld, tld_from_domain)
} else {
return Err("You must specify either a domain to check or a file with domains (--file)".into());
};
if !args.json && (args.debug || args.verbose) {
if args.file.is_some() {
if args.pretty {
println!("đ Checking {} domains from file", domains.len());
} else {
println!("Checking {} domains from file...", domains.len());
}
if args.ui {
println!("Interactive UI will be shown after processing completes.");
}
let concurrency = args.concurrency.min(100);
println!("Using concurrency: {} - Please wait...\n", concurrency);
if args.verbose && args.info && args.pretty {
println!("âšī¸ Detailed info will be shown for taken domains\n");
}
} else {
if args.pretty {
let base_name = domains.first().map(|d| d.split('.').next().unwrap_or("")).unwrap_or("");
print!("đ Checking: {}", base_name);
let tlds_str = domains.iter().filter_map(|d| d.split('.').nth(1)).collect::<Vec<_>>().join(", ");
println!(" with TLDs: {}", tlds_str);
if args.verbose && args.info {
println!("âšī¸ Detailed info will be shown for taken domains\n");
}
} else {
println!("Checking: {}\n", domains.join(", "));
if args.ui {
println!("Interactive UI will be shown after processing completes.");
}
}
}
}
let results = if args.file.is_some() {
check_domains_in_bulk(domains, args.clone(), registry_map).await
} else {
check_domains_in_parallel(domains, args.clone(), registry_map).await
};
if args.json {
let json = serde_json::to_string_pretty(&results).unwrap();
println!("\n{}", json);
}
if args.ui && !results.is_empty() {
display_interactive_dashboard(&results)?;
}
Ok(())
}