rdap 0.2.0

A modern RDAP (Registration Data Access Protocol) client
Documentation
//! RDAP client implementation
//!
//! Provides [`RdapClient`], the primary entry point for executing RDAP queries.
//! The client handles HTTP transport, IANA bootstrap service discovery, and
//! automatic JSON response parsing into typed [`RdapObject`] values.

use crate::bootstrap::BootstrapClient;
use crate::error::{RdapError, Result};
use crate::models::RdapObject;
use crate::request::RdapRequest;
use reqwest::Client;
use std::time::Duration;
use url::Url;

/// HTTP client for executing RDAP queries.
///
/// Wraps an HTTP client and a [`BootstrapClient`] for automatic server
/// discovery. Create one with [`RdapClient::new`], optionally configure
/// the timeout with [`with_timeout`](RdapClient::with_timeout), then call
/// [`query`](RdapClient::query) to perform lookups.
///
/// # Default timeout
///
/// 30 seconds. Override with [`with_timeout`](RdapClient::with_timeout).
pub struct RdapClient {
    http_client: Client,
    bootstrap: BootstrapClient,
    timeout: Duration,
}

impl RdapClient {
    /// Create a new RDAP client with default settings.
    ///
    /// Uses a 30-second timeout, `rustls` TLS backend, and the `rdap-rust/<version>`
    /// user-agent string.
    pub fn new() -> Result<Self> {
        let http_client = Client::builder()
            .timeout(Duration::from_secs(30))
            .user_agent(concat!("rdap-rust/", env!("CARGO_PKG_VERSION")))
            .build()?;

        let bootstrap = BootstrapClient::new()?;

        Ok(Self {
            http_client,
            bootstrap,
            timeout: Duration::from_secs(30),
        })
    }

    /// Set the HTTP request timeout.
    ///
    /// The default is 30 seconds. The timeout applies to each individual
    /// HTTP request, not the total query time (which may involve multiple
    /// server attempts).
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    /// Execute an RDAP query and return the parsed response.
    ///
    /// If `request.server` is set, queries that server directly. Otherwise,
    /// uses IANA bootstrap to discover candidate servers and tries each in
    /// order until one succeeds.
    ///
    /// Returns [`RdapError::NotFound`] immediately (without trying other
    /// servers) when any server returns HTTP 404.
    pub async fn query(&self, request: &RdapRequest) -> Result<RdapObject> {
        // Determine RDAP servers
        let urls = if let Some(server) = &request.server {
            vec![server.clone()]
        } else {
            self.bootstrap.lookup(request).await?
        };

        if urls.is_empty() {
            return Err(RdapError::Bootstrap("No RDAP servers found".to_string()));
        }

        // Try each server
        let mut last_error = None;

        for base_url in &urls {
            let url = request.build_url(base_url)?;

            log::debug!("Querying RDAP server: {}", url);

            match self.fetch_rdap(&url).await {
                Ok(obj) => return Ok(obj),
                Err(RdapError::NotFound) => return Err(RdapError::NotFound),
                Err(e) => {
                    log::warn!("Server {} failed: {}", url, e);
                    last_error = Some(e);
                }
            }
        }

        Err(last_error.unwrap_or(RdapError::NoWorkingServers))
    }

    /// Fetch and parse an RDAP response from a fully-resolved URL.
    async fn fetch_rdap(&self, url: &Url) -> Result<RdapObject> {
        let response = self
            .http_client
            .get(url.as_str())
            .header("Accept", "application/rdap+json, application/json")
            .send()
            .await?;

        let status = response.status();

        if status.is_success() {
            let text = response.text().await?;
            let obj = self.parse_response(&text)?;
            Ok(obj)
        } else if status.as_u16() == 404 {
            Err(RdapError::NotFound)
        } else {
            // Try to parse as error response
            let text = response.text().await?;
            if let Ok(err_obj) = serde_json::from_str::<crate::models::ErrorResponse>(&text) {
                Err(RdapError::ServerError {
                    code: err_obj.error_code.unwrap_or(status.as_u16()),
                    title: err_obj.title.unwrap_or_else(|| "Unknown error".to_string()),
                    description: err_obj.description,
                })
            } else {
                Err(RdapError::Other(format!("HTTP error: {}", status)))
            }
        }
    }

    /// Parse an RDAP JSON response into the appropriate [`RdapObject`] variant.
    ///
    /// Detection order: error → search results → `objectClassName` → help.
    fn parse_response(&self, json: &str) -> Result<RdapObject> {
        // First, parse as generic JSON to inspect structure
        let value: serde_json::Value = serde_json::from_str(json)?;

        // Detect object type
        if let Some(obj) = value.as_object() {
            // Check for error
            if obj.contains_key("errorCode") {
                return Ok(RdapObject::Error(serde_json::from_value(value)?));
            }

            // Check for search results
            if obj.contains_key("domainSearchResults") {
                return Ok(RdapObject::DomainSearch(serde_json::from_value(value)?));
            }
            if obj.contains_key("entitySearchResults") {
                return Ok(RdapObject::EntitySearch(serde_json::from_value(value)?));
            }
            if obj.contains_key("nameserverSearchResults") {
                return Ok(RdapObject::NameserverSearch(serde_json::from_value(value)?));
            }

            // Check objectClassName
            if let Some(class_name) = obj.get("objectClassName").and_then(|v| v.as_str()) {
                match class_name {
                    "domain" => return Ok(RdapObject::Domain(serde_json::from_value(value)?)),
                    "entity" => return Ok(RdapObject::Entity(serde_json::from_value(value)?)),
                    "nameserver" => {
                        return Ok(RdapObject::Nameserver(serde_json::from_value(value)?));
                    }
                    "autnum" => return Ok(RdapObject::Autnum(serde_json::from_value(value)?)),
                    "ip network" => {
                        return Ok(RdapObject::IpNetwork(serde_json::from_value(value)?));
                    }
                    _ => {}
                }
            }

            // Default to Help
            Ok(RdapObject::Help(serde_json::from_value(value)?))
        } else {
            Err(RdapError::Json(serde::de::Error::custom(
                "Invalid RDAP response",
            )))
        }
    }
}

/// Creates an [`RdapClient`] with default settings.
///
/// # Panics
///
/// Panics if the underlying HTTP client cannot be created.
impl Default for RdapClient {
    fn default() -> Self {
        Self::new().expect("Failed to create RDAP client")
    }
}