WHOIS & RDAP Lookup — Python source
Look up registration data for any domain, IP address, or ASN — straight from your browser to the authoritative RDAP registry over HTTPS. No backend, no database of lookups, fully private.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""RDAP lookup — WHOIS registration data over HTTPS (RFCs 7480-7485).
Language: Python (standard library only)
CosmoDev polyglot showcase port of the ``whois`` tool.
Ported from src/lib/whois.ts — display source, part of CosmoDev's
polyglot tool pages.
RDAP is the JSON successor to port-43 WHOIS. The client first asks
IANA's bootstrap registry which server is authoritative for the TLD
(dns.json) or the ASN block (asn.json), then fetches the record
straight from that server — two HTTPS GETs, no WHOIS port, no backend.
"""
from __future__ import annotations
import json
import re
import urllib.request
BOOTSTRAP_URLS = {
"domain": "https://data.iana.org/RDAP/dns.json",
"autnum": "https://data.iana.org/RDAP/asn.json",
}
def classify(query: str) -> dict:
"""Classify a query: an AS number ("as3356" / "3356") or a domain name."""
m = re.fullmatch(r"(?:as)?(\d{1,10})", query.strip(), re.IGNORECASE)
if m and int(m.group(1)) > 0:
return {"kind": "autnum", "value": str(int(m.group(1)))}
value = query.strip().lower().rstrip(".")
return {"kind": "domain", "value": value, "tld": value.rsplit(".", 1)[-1]}
def asn_in_range(key: str, asn: int) -> bool:
"""True when ``asn`` falls in an asn.json key ("lo-hi" or a bare number)."""
if "-" in key:
lo, hi = (int(n) for n in key.split("-", 1))
return lo <= asn <= hi
return int(key) == asn
def fetch_json(url: str):
"""GET a URL over HTTPS and parse the JSON body (RDAP speaks both)."""
with urllib.request.urlopen(url, timeout=15) as res:
return json.loads(res.read().decode())
def resolve_rdap_base(q: dict) -> str:
"""Ask IANA's bootstrap which RDAP server is authoritative for the query."""
services = fetch_json(BOOTSTRAP_URLS[q["kind"]])["services"]
for keys, urls in services:
if q["kind"] == "autnum":
hit = any(asn_in_range(k, int(q["value"])) for k in keys)
else:
hit = any(k.lower() == q["tld"] for k in keys)
if hit:
return urls[0]
raise LookupError(f"no RDAP server for {q['value']}")
def whois_lookup(query: str) -> dict:
"""Look up a domain or ASN over RDAP.
Returns the essentials: registrar (name + email), registration and
expiration dates, status flags, and nameservers. Raises
:class:`LookupError` for an unknown TLD/ASN and :class:`OSError` /
:class:`ValueError` on network or JSON failures.
"""
q = classify(query)
base = resolve_rdap_base(q)
rdap = fetch_json(f"{base.rstrip('/')}/{q['kind']}/{q['value']}")
# Dates are event objects: [{"eventAction": "registration", ...}, ...]
dates = {
e.get("eventAction", "").lower(): e.get("eventDate")
for e in rdap.get("events", [])
}
# The registrar is an entity with role "registrar"; its name and email
# live inside the jCard array as ["fn", {}, "text", "..."] properties.
registrar = next(
(e for e in rdap.get("entities", []) if "registrar" in e.get("roles", [])),
{},
)
def vcard(field: str) -> str | None:
for prop in registrar.get("vcardArray", [None, []])[1]:
if prop[0] == field:
return prop[3]
return None
return {
"registrar": vcard("fn"),
"registrarEmail": vcard("email"),
"registered": dates.get("registration"),
"expires": dates.get("expiration"),
"status": rdap.get("status", []),
"nameservers": [ns["ldhName"] for ns in rdap.get("nameservers", []) if "ldhName" in ns],
}
if __name__ == "__main__":
print(json.dumps(whois_lookup("cosmolabs.org"), indent=2))
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →