"""Check whether a domain looks registered or potentially available to buy.
This script combines two simple signals:
1. RDAP lookup to see whether the registry reports the domain as registered.
2. DNS resolution to see whether the domain currently has common records in use.
It is still only a heuristic. A registrar is the final source of truth for whether
you can actually buy a domain right now.
"""
from __future__ import annotations
import argparse
import json
import socket
import sys
from dataclasses import dataclass
from typing import Iterable
from urllib import error, request
RDAP_URL_TEMPLATE = "https://rdap.org/domain/{domain}"
USER_AGENT = "domain-checker/1.0"
@dataclass()
class DomainCheckResult:
domain: str
rdap_status: str
dns_status: str
likely_available: bool
registrar_hint: str | None
notes: list[str]
def normalize_domain(domain: str) -> str:
cleaned = domain.strip().lower()
if cleaned.startswith(("http://", "https://")):
cleaned = cleaned.split("://", 1)[1]
cleaned = cleaned.split("/", 1)[0]
cleaned = cleaned.removesuffix(".")
# Add .com if no TLD is present
if "." not in cleaned:
cleaned += ".com"
return cleaned
def check_rdap(domain: str, timeout: float) -> tuple[str, str | None, list[str]]:
req = request.Request(
RDAP_URL_TEMPLATE.format(domain=domain),
headers={"User-Agent": USER_AGENT, "Accept": "application/rdap+json, application/json"},
)
try:
with request.urlopen(req, timeout=timeout) as response:
payload = json.load(response)
except error.HTTPError as exc:
if exc.code == 404:
return "not found", None, ["Registry RDAP returned 404."]
return "error", None, [f"RDAP HTTP error: {exc.code}."]
except error.URLError as exc:
return "error", None, [f"RDAP network error: {exc.reason}."]
except TimeoutError:
return "error", None, ["RDAP request timed out."]
except json.JSONDecodeError:
return "error", None, ["RDAP response was not valid JSON."]
registrar_hint = None
notes: list[str] = []
entities = payload.get("entities", [])
for entity in entities:
roles = entity.get("roles", [])
if "registrar" not in roles:
continue
vcard_array = entity.get("vcardArray", [])
if len(vcard_array) < 2:
continue
for card in vcard_array[1]:
if len(card) >= 4 and card[0] == "fn":
registrar_hint = str(card[3])
break
if registrar_hint:
break
status_values = payload.get("status") or []
if status_values:
notes.append(f"RDAP status: {', '.join(map(str, status_values))}.")
ldh_name = payload.get("ldhName")
if ldh_name:
notes.append(f"Registry returned record for {ldh_name}.")
return "registered", registrar_hint, notes
def check_dns(domain: str) -> tuple[str, list[str]]:
notes: list[str] = []
record_types = ("80", "443")
try:
answers = socket.getaddrinfo(domain, None, proto=socket.IPPROTO_TCP)
except socket.gaierror as exc:
return "not resolving", [f"DNS resolution failed: {exc}."]
addresses = sorted({entry[4][0] for entry in answers})
if addresses:
notes.append(f"Resolved addresses: {', '.join(addresses[:4])}.")
# Try common subdomain patterns as a soft signal for live configuration.
for port in record_types:
try:
with socket.create_connection((domain, int(port)), timeout=1.5):
notes.append(f"Accepted TCP connection on port {port}.")
return "active", notes
except OSError:
continue
return "resolving", notes
def evaluate_domain(domain: str, timeout: float) -> DomainCheckResult:
notes: list[str] = []
rdap_status, registrar_hint, rdap_notes = check_rdap(domain, timeout)
dns_status, dns_notes = check_dns(domain)
notes.extend(rdap_notes)
notes.extend(dns_notes)
likely_available = rdap_status == "not found" and dns_status == "not resolving"
if likely_available:
notes.append("No registry record and no DNS resolution were found.")
elif rdap_status == "registered":
notes.append("Registry data indicates the domain is already registered.")
else:
notes.append("Availability is uncertain; verify with a registrar before buying.")
return DomainCheckResult(
domain=domain,
rdap_status=rdap_status,
dns_status=dns_status,
likely_available=likely_available,
registrar_hint=registrar_hint,
notes=notes,
)
def parse_args(argv: Iterable[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Check whether a domain looks registered or potentially available to buy.",
)
parser.add_argument("domains", nargs="+", help="One or more domains to inspect.")
parser.add_argument(
"--timeout",
type=float,
default=5.0,
help="Network timeout in seconds for the RDAP request. Default: 5.0",
)
return parser.parse_args(list(argv))
def print_result(result: DomainCheckResult) -> None:
verdict = "POSSIBLY AVAILABLE" if result.likely_available else "LIKELY REGISTERED / UNCERTAIN"
print(f"\nDomain: {result.domain}")
print(f"Verdict: {verdict}")
print(f"RDAP: {result.rdap_status}")
print(f"DNS: {result.dns_status}")
if result.registrar_hint:
print(f"Registrar: {result.registrar_hint}")
print("Notes:")
for note in result.notes:
print(f" - {note}")
def main(argv: Iterable[str]) -> int:
args = parse_args(argv)
for raw_domain in args.domains:
domain = normalize_domain(raw_domain)
if not domain:
print(f"\nSkipping invalid input: {raw_domain!r}")
continue
print_result(evaluate_domain(domain, args.timeout))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))