dnsrobot 0.1.0

Official Rust client for DNS Robot (dnsrobot.net) — DNS lookups, WHOIS, SSL checks, SPF/DKIM/DMARC validation, and more
Documentation
//! # DnsRobot
//!
//! Official Rust client for [DNS Robot](https://dnsrobot.net) — 53 free online DNS and network tools.
//!
//! Zero external dependencies. Uses only `std::net::TcpStream` for HTTP requests.
//!
//! ## Usage
//!
//! ```no_run
//! use dnsrobot::DnsRobotClient;
//!
//! let client = DnsRobotClient::new();
//! let result = client.dns_lookup("example.com", Some("A"), Some("8.8.8.8")).unwrap();
//! println!("{}", result);
//! ```

use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::TcpStream;

/// Client for the DNS Robot API.
///
/// See <https://dnsrobot.net> for the full list of tools.
pub struct DnsRobotClient {
    base_url: String,
    host: String,
    user_agent: String,
}

impl Default for DnsRobotClient {
    fn default() -> Self {
        Self::new()
    }
}

impl DnsRobotClient {
    /// Creates a new client with the default API URL.
    pub fn new() -> Self {
        Self {
            base_url: "https://dnsrobot.net/api".to_string(),
            host: "dnsrobot.net".to_string(),
            user_agent: "dnsrobot-rust/0.1.0".to_string(),
        }
    }

    /// Creates a new client with a custom base URL.
    pub fn with_base_url(base_url: &str) -> Self {
        let host = base_url
            .replace("https://", "")
            .replace("http://", "")
            .split('/')
            .next()
            .unwrap_or("dnsrobot.net")
            .to_string();
        Self {
            base_url: base_url.trim_end_matches('/').to_string(),
            host,
            user_agent: "dnsrobot-rust/0.1.0".to_string(),
        }
    }

    /// DNS record lookup.
    ///
    /// See <https://dnsrobot.net/dns-lookup>
    pub fn dns_lookup(&self, domain: &str, record_type: Option<&str>, dns_server: Option<&str>) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        body.insert("recordType", record_type.unwrap_or("A"));
        body.insert("dnsServer", dns_server.unwrap_or("8.8.8.8"));
        self.post("dns-query", &body)
    }

    /// WHOIS domain registration lookup.
    ///
    /// See <https://dnsrobot.net/whois-lookup>
    pub fn whois_lookup(&self, domain: &str) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        self.post("whois", &body)
    }

    /// SSL/TLS certificate check.
    ///
    /// See <https://dnsrobot.net/ssl-checker>
    pub fn ssl_check(&self, domain: &str) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        self.post("ssl-certificate", &body)
    }

    /// SPF record validation.
    ///
    /// See <https://dnsrobot.net/spf-checker>
    pub fn spf_check(&self, domain: &str) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        self.post("spf-checker", &body)
    }

    /// DKIM record check.
    ///
    /// See <https://dnsrobot.net/dkim-checker>
    pub fn dkim_check(&self, domain: &str, selector: Option<&str>) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        if let Some(sel) = selector {
            body.insert("selector", sel);
        }
        self.post("dkim-checker", &body)
    }

    /// DMARC record check.
    ///
    /// See <https://dnsrobot.net/dmarc-checker>
    pub fn dmarc_check(&self, domain: &str) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        self.post("dmarc-checker", &body)
    }

    /// MX record lookup.
    ///
    /// See <https://dnsrobot.net/mx-lookup>
    pub fn mx_lookup(&self, domain: &str) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        self.post("mx-lookup", &body)
    }

    /// Nameserver lookup.
    ///
    /// See <https://dnsrobot.net/ns-lookup>
    pub fn ns_lookup(&self, domain: &str) -> Result<String, String> {
        if domain.is_empty() {
            return Err("domain is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("domain", domain);
        self.post("ns-lookup", &body)
    }

    /// IP geolocation lookup.
    ///
    /// See <https://dnsrobot.net/ip-lookup>
    pub fn ip_lookup(&self, ip: &str) -> Result<String, String> {
        if ip.is_empty() {
            return Err("ip is required".to_string());
        }
        let mut body = HashMap::new();
        body.insert("ip", ip);
        self.post("ip-info", &body)
    }

    /// HTTP response headers check.
    ///
    /// See <https://dnsrobot.net/http-headers-checker>
    pub fn http_headers(&self, url: &str) -> Result<String, String> {
        if url.is_empty() {
            return Err("url is required".to_string());
        }
        let full_url = if url.starts_with("http://") || url.starts_with("https://") {
            url.to_string()
        } else {
            format!("https://{}", url)
        };
        let mut body = HashMap::new();
        body.insert("url", full_url.as_str());
        self.post("http-headers", &body)
    }

    /// Port availability check.
    ///
    /// See <https://dnsrobot.net/port-checker>
    pub fn port_check(&self, host: &str, port: u16) -> Result<String, String> {
        if host.is_empty() {
            return Err("host is required".to_string());
        }
        let path = format!("/api/port-check?host={}&port={}", host, port);
        self.get(&path)
    }

    fn post(&self, endpoint: &str, body: &HashMap<&str, &str>) -> Result<String, String> {
        let json = dict_to_json(body);
        let path = format!("/api/{}", endpoint);

        let request = format!(
            "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nUser-Agent: {}\r\nConnection: close\r\n\r\n{}",
            path, self.host, json.len(), self.user_agent, json
        );

        self.send_request(&request)
    }

    fn get(&self, path: &str) -> Result<String, String> {
        let request = format!(
            "GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: {}\r\nConnection: close\r\n\r\n",
            path, self.host, self.user_agent
        );

        self.send_request(&request)
    }

    fn send_request(&self, request: &str) -> Result<String, String> {
        let use_tls = self.base_url.starts_with("https://");
        let port = if use_tls { 443 } else { 80 };
        let addr = format!("{}:{}", self.host, port);

        if use_tls {
            return Err("HTTPS requires a TLS library. Use ureq or reqwest for production.".to_string());
        }

        let mut stream = TcpStream::connect(&addr).map_err(|e| e.to_string())?;
        stream.write_all(request.as_bytes()).map_err(|e| e.to_string())?;

        let mut response = String::new();
        stream.read_to_string(&mut response).map_err(|e| e.to_string())?;

        if let Some(pos) = response.find("\r\n\r\n") {
            Ok(response[pos + 4..].to_string())
        } else {
            Ok(response)
        }
    }
}

fn dict_to_json(dict: &HashMap<&str, &str>) -> String {
    let entries: Vec<String> = dict
        .iter()
        .map(|(k, v)| format!("\"{}\":\"{}\"", escape_json(k), escape_json(v)))
        .collect();
    format!("{{{}}}", entries.join(","))
}

fn escape_json(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_client() {
        let client = DnsRobotClient::new();
        assert_eq!(client.base_url, "https://dnsrobot.net/api");
        assert_eq!(client.host, "dnsrobot.net");
    }

    #[test]
    fn test_custom_base_url() {
        let client = DnsRobotClient::with_base_url("http://localhost:3000/api");
        assert_eq!(client.host, "localhost:3000");
    }

    #[test]
    fn test_default_trait() {
        let client = DnsRobotClient::default();
        assert_eq!(client.base_url, "https://dnsrobot.net/api");
    }

    #[test]
    fn test_dns_lookup_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.dns_lookup("", None, None).is_err());
    }

    #[test]
    fn test_whois_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.whois_lookup("").is_err());
    }

    #[test]
    fn test_ssl_check_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.ssl_check("").is_err());
    }

    #[test]
    fn test_spf_check_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.spf_check("").is_err());
    }

    #[test]
    fn test_dkim_check_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.dkim_check("", None).is_err());
    }

    #[test]
    fn test_dmarc_check_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.dmarc_check("").is_err());
    }

    #[test]
    fn test_mx_lookup_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.mx_lookup("").is_err());
    }

    #[test]
    fn test_ns_lookup_empty_domain() {
        let client = DnsRobotClient::new();
        assert!(client.ns_lookup("").is_err());
    }

    #[test]
    fn test_ip_lookup_empty() {
        let client = DnsRobotClient::new();
        assert!(client.ip_lookup("").is_err());
    }

    #[test]
    fn test_http_headers_empty() {
        let client = DnsRobotClient::new();
        assert!(client.http_headers("").is_err());
    }

    #[test]
    fn test_port_check_empty() {
        let client = DnsRobotClient::new();
        assert!(client.port_check("", 80).is_err());
    }

    #[test]
    fn test_json_serialization() {
        let mut map = HashMap::new();
        map.insert("key", "value");
        let json = dict_to_json(&map);
        assert!(json.contains("\"key\":\"value\""));
    }

    #[test]
    fn test_escape_json() {
        assert_eq!(escape_json("hello\"world"), "hello\\\"world");
        assert_eq!(escape_json("line\nnew"), "line\\nnew");
    }
}