WHOIS & RDAP Lookup — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! RDAP lookup — WHOIS registration data over HTTPS (RFCs 7480-7485).
//!
//! Language: Rust
//! 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: ask IANA's bootstrap
//! registry which server is authoritative for the TLD (dns.json) or the
//! ASN block (asn.json), then fetch the record straight from that
//! server — two HTTPS GETs, no WHOIS port, no backend.
//!
//! Rust ships no HTTP client in the standard library, so this port uses
//! `reqwest` (blocking) + `serde_json`. (Cargo.toml:
//! `reqwest = { version = "0.12", features = ["blocking"] }`,
//! `serde_json = "1"`.)
use std::time::Duration;
use reqwest::blocking::Client;
use serde_json::{json, Value};
const BOOTSTRAP_DOMAIN: &str = "https://data.iana.org/RDAP/dns.json";
const BOOTSTRAP_AUTNUM: &str = "https://data.iana.org/RDAP/asn.json";
/// A classified lookup target: a domain name or an AS number.
enum Query {
Domain { name: String, tld: String },
Autnum(u32),
}
fn classify(raw: &str) -> Option<Query> {
let q = raw.trim();
// Accept an optional "AS"/"as" prefix before the number.
let bare = match q.get(..2) {
Some(p) if p.eq_ignore_ascii_case("as") => &q[2..],
_ => q,
};
if !bare.is_empty() && bare.bytes().all(|b| b.is_ascii_digit()) {
let asn: u32 = bare.parse().ok()?;
if asn > 0 {
return Some(Query::Autnum(asn));
}
}
let name = q.to_ascii_lowercase();
let name = name.trim_end_matches('.');
let tld = name.rsplit('.').next()?.to_ascii_lowercase();
if tld.is_empty() {
return None;
}
Some(Query::Domain { name: name.to_string(), tld })
}
/// True when `asn` falls inside an asn.json key ("lo-hi" or a bare number).
fn asn_in_range(key: &str, asn: u32) -> bool {
match key.split_once('-') {
Some((lo, hi)) => match (lo.parse::<u32>(), hi.parse::<u32>()) {
(Ok(lo), Ok(hi)) => lo <= asn && asn <= hi,
_ => false,
},
None => key.parse::<u32>() == Ok(asn),
}
}
/// Ask IANA's bootstrap registry which RDAP server is authoritative.
fn resolve_base(client: &Client, query: &Query) -> Option<String> {
let url = match query {
Query::Domain { .. } => BOOTSTRAP_DOMAIN,
Query::Autnum(_) => BOOTSTRAP_AUTNUM,
};
let doc: Value = client.get(url).send().ok()?.json().ok()?;
for service in doc["services"].as_array()? {
let (Some(keys), Some(urls)) = (service[0].as_array(), service[1].as_array()) else {
continue;
};
let hit = match query {
Query::Domain { tld, .. } => keys.iter().any(|k| {
k.as_str().is_some_and(|k| k.eq_ignore_ascii_case(tld))
}),
Query::Autnum(asn) => keys
.iter()
.any(|k| k.as_str().is_some_and(|k| asn_in_range(k, *asn))),
};
if hit {
return urls[0].as_str().map(str::to_string);
}
}
None
}
/// The eventDate of one action ("registration", "expiration"), if present.
fn event_date(rdap: &Value, action: &str) -> Option<String> {
rdap["events"]
.as_array()?
.iter()
.find(|e| e["eventAction"].as_str() == Some(action))
.and_then(|e| e["eventDate"].as_str().map(str::to_string))
}
/// Pull one property ("fn" name, "email") out of an entity's jCard array.
fn vcard_field(entity: &Value, field: &str) -> Option<String> {
let props = entity["vcardArray"].as_array()?.get(1)?.as_array()?;
let prop = props.iter().find(|p| {
p.as_array()
.and_then(|a| a.first())
.and_then(|v| v.as_str())
== Some(field)
})?;
prop.as_array()?.get(3)?.as_str().map(str::to_string)
}
/// Look up a domain or ASN over RDAP: registrar, dates, status, nameservers.
fn whois_lookup(client: &Client, raw: &str) -> Result<Value, Box<dyn std::error::Error>> {
let query = classify(raw).ok_or("invalid query")?;
let base = resolve_base(client, &query).ok_or("no RDAP server for query")?;
let (path, object) = match &query {
Query::Domain { name, .. } => ("domain", name.clone()),
Query::Autnum(asn) => ("autnum", asn.to_string()),
};
let url = format!("{}/{}/{}", base.trim_end_matches('/'), path, object);
let rdap: Value = client.get(url).send()?.error_for_status()?.json()?;
// The registrar is the entity with role "registrar"; its name and
// email live inside the jCard array.
let registrar = rdap["entities"].as_array().and_then(|ents| {
ents.iter().find(|e| {
e["roles"].as_array().is_some_and(|r| {
r.iter().any(|x| x.as_str() == Some("registrar"))
})
})
});
let nameservers: Vec<String> = rdap["nameservers"]
.as_array()
.map(|ns| {
ns.iter()
.filter_map(|n| n["ldhName"].as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
Ok(json!({
"registrar": registrar.and_then(|e| vcard_field(e, "fn")),
"registrarEmail": registrar.and_then(|e| vcard_field(e, "email")),
"registered": event_date(&rdap, "registration"),
"expires": event_date(&rdap, "expiration"),
"status": rdap["status"].clone(),
"nameservers": nameservers,
}))
}
fn main() {
// 15s timeout: RDAP servers are authoritative but occasionally slow.
let client = Client::builder()
.timeout(Duration::from_secs(15))
.build()
.unwrap();
match whois_lookup(&client, "cosmolabs.org") {
Ok(record) => println!("{record:#}"),
Err(e) => eprintln!("error: {e}"),
}
}
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 →