use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::TcpStream;
pub struct DnsRobotClient {
base_url: String,
host: String,
user_agent: String,
}
impl Default for DnsRobotClient {
fn default() -> Self {
Self::new()
}
}
impl DnsRobotClient {
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(),
}
}
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(),
}
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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");
}
}