Skip to content

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 →