monovm_whois/lib.rs
1//! Domain WHOIS and RDAP lookups, with availability detection you can audit.
2//!
3//! ```no_run
4//! # #[cfg(feature = "blocking")] {
5//! use monovm_whois::WhoisClient;
6//!
7//! let client = WhoisClient::new()?;
8//! let lookup = client.lookup("example.com")?;
9//!
10//! println!("{} is {}", lookup.domain, lookup.availability());
11//! if let Some(record) = &lookup.record {
12//! println!("registrar: {:?}", record.registrar);
13//! println!("expires: {:?}", record.expires);
14//! }
15//! # }
16//! # Ok::<(), monovm_whois::Error>(())
17//! ```
18//!
19//! # The problem this crate is about
20//!
21//! WHOIS has no status codes. A registry answering "that domain is free", one
22//! answering "you are querying too fast", and one answering "I do not serve that
23//! suffix" all send prose over the same socket, and every one of them can contain
24//! the word *available*. Libraries in this space overwhelmingly resolve that
25//! ambiguity the same way — anything that is not recognisably a record is treated as
26//! availability — which means a rate-limited registry reports its entire zone as
27//! free to register.
28//!
29//! This crate never does that. A response that cannot be interpreted produces
30//! [`Error::Inconclusive`], a refusal produces [`Error::Refused`], and neither is
31//! ever an [`Availability`]. There is deliberately no `Availability::Unknown`,
32//! because an uncertain answer that renders as "available" is the one outcome a
33//! caller must not be handed.
34//!
35//! # How it is put together
36//!
37//! Six layers, each with one job and no knowledge of the others:
38//!
39//! | Layer | Responsibility | Key abstraction |
40//! |---|---|---|
41//! | [`domain`] | Validated values — a name, a suffix, a verdict | [`DomainName`], [`Tld`] |
42//! | [`registry`] | Which registry serves a suffix, and how to reach it | [`RegistryProvider`](registry::RegistryProvider) |
43//! | [`transport`] | Talking to servers. The only I/O in the crate | [`Transport`](transport::Transport) |
44//! | [`cache`] | Not asking twice | [`ResponseCache`](cache::ResponseCache) |
45//! | [`detect`] | Deciding what a response said | [`AvailabilityRule`](detect::AvailabilityRule) |
46//! | [`parser`] | Turning a record into data | [`RecordParser`](parser::RecordParser) |
47//!
48//! [`client`] composes them. Every layer is a trait with a bundled implementation,
49//! so a caller can replace any one of them — a private registry list, a transport
50//! over a proxy, a Redis cache, an extra detection rule for a registry that words
51//! things unusually — without forking the crate.
52//!
53//! # What you get
54//!
55//! - **Coverage.** 872 curated suffixes plus IANA's RDAP bootstrap registry, for
56//! over 1600 in total.
57//! - **RDAP.** A full RFC 9083 client and typed model, used as a fallback when port
58//! 43 refuses and preferred when [`Preference::Rdap`] is set. RDAP's 404 makes availability a fact rather than an inference.
59//! - **Structured records.** [`WhoisRecord`] with typed dates,
60//! statuses, name servers and contacts, instead of the server's raw text.
61//! - **Referral chasing.** Thin registries answer with a pointer to the registrar;
62//! following it is the difference between knowing a domain is taken and knowing
63//! who holds it.
64//! - **Auditable verdicts.** Every answer names the rule that produced it and why,
65//! and [`WhoisClient::explain`] shows what every rule thought.
66//! - **Rate limiting, retries and caching**, composed as transport decorators.
67//! - **Both runtimes.** [`WhoisClient`] and [`AsyncWhoisClient`].
68//!
69//! # Features
70//!
71//! | Feature | Default | Gives you |
72//! |---|---|---|
73//! | `blocking` | yes | [`WhoisClient`] and the synchronous transports |
74//! | `rdap` | yes | RDAP over HTTPS, and the typed [`rdap`] model |
75//! | `parser` | yes | [`WhoisRecord`] and record parsing |
76//! | `async` | no | [`AsyncWhoisClient`] and the Tokio transports |
77//! | `iana-bootstrap` | no | Refreshing the RDAP registry from IANA at runtime |
78//! | `cli` | no | The `monovm-whois` command line tool |
79//! | `mock` | no | [`MockTransport`](transport::MockTransport), for your own tests |
80//!
81//! # A note on what a verdict means
82//!
83//! Availability detection over WHOIS is inference, and this crate is explicit about
84//! how much. Every [`Verdict`](detect::Verdict) carries a
85//! [`Confidence`](detect::Confidence): `Definitive` for a structured RDAP answer,
86//! `High` for wording curated for that specific registry, `Medium` for a pattern
87//! that generalises, `Low` for the one inference drawn from absence of evidence. A
88//! caller who needs certainty can require `Definitive` and use
89//! [`Preference::RdapOnly`].
90
91#![cfg_attr(docsrs, feature(doc_cfg))]
92#![warn(missing_docs)]
93#![warn(clippy::all)]
94#![forbid(unsafe_code)]
95
96pub mod cache;
97pub mod client;
98pub mod detect;
99pub mod domain;
100pub mod error;
101pub mod registry;
102pub mod transport;
103
104#[cfg(feature = "parser")]
105pub mod parser;
106
107#[cfg(feature = "rdap")]
108pub mod rdap;
109
110pub use domain::{Availability, DomainName, SuffixSplit, Tld};
111pub use error::{DomainError, Error, Refusal, Result};
112
113pub use client::{Explanation, Lookup, Preference, ReferralPolicy};
114
115#[cfg(any(feature = "blocking", feature = "async"))]
116pub use client::CheckReport;
117
118#[cfg(feature = "blocking")]
119pub use client::{Checker, WhoisClient, WhoisClientBuilder};
120
121#[cfg(feature = "async")]
122pub use client::{AsyncChecker, AsyncWhoisClient, AsyncWhoisClientBuilder};
123
124#[cfg(feature = "parser")]
125pub use parser::{Contact, WhoisRecord};
126
127/// The crate version, from `Cargo.toml`.
128pub const VERSION: &str = env!("CARGO_PKG_VERSION");
129
130/// Look one domain up with a default client.
131///
132/// A convenience for a one-off query. Anything repeated should build a
133/// [`WhoisClient`] and keep it: a client carries the rate limiter and the cache, and
134/// a fresh one per query has neither.
135///
136/// ```no_run
137/// # #[cfg(feature = "blocking")] {
138/// let lookup = monovm_whois::lookup("example.com")?;
139/// println!("{}", lookup.availability());
140/// # }
141/// # Ok::<(), monovm_whois::Error>(())
142/// ```
143#[cfg(feature = "blocking")]
144pub fn lookup(domain: &str) -> Result<Lookup> {
145 WhoisClient::new()?.lookup(domain)
146}
147
148/// Whether one domain is free to register, with a default client.
149///
150/// A premium or reserved name answers `false`; a query that could not be answered is
151/// an error rather than `false`.
152#[cfg(feature = "blocking")]
153pub fn is_available(domain: &str) -> Result<bool> {
154 WhoisClient::new()?.is_available(domain)
155}
156
157/// The availability of one domain, with a default client.
158#[cfg(feature = "blocking")]
159pub fn availability(domain: &str) -> Result<Availability> {
160 WhoisClient::new()?.availability(domain)
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn the_version_is_populated() {
169 assert!(!VERSION.is_empty());
170 assert!(VERSION.contains('.'));
171 }
172
173 #[test]
174 fn the_bundled_data_covers_what_the_docs_claim() {
175 let registry = registry::default_provider();
176 let total = registry::RegistryProvider::tlds(®istry).len();
177
178 assert!(
179 total > 1600,
180 "the crate documents over 1600 suffixes; found {total}"
181 );
182 }
183
184 #[test]
185 fn the_prelude_reexports_resolve() {
186 // A compile-level check that the public surface named in the crate docs
187 // actually exists at these paths.
188 let _: fn(&str) -> Result<DomainName> = |s| DomainName::parse(s);
189 let _: fn(&str) -> Result<Tld> = |s| Tld::parse(s);
190 assert!(Availability::Available.is_available());
191 assert_eq!(Preference::default(), Preference::Whois);
192 assert!(ReferralPolicy::default().is_enabled());
193 }
194}