use std::error::Error;
use std::fmt;
use quick_xml::events::Event;
use quick_xml::Reader;
use reqwest::Client as HttpClient;
use crate::types::Environment;
use crate::HttpClientConfig;
const PRODUCTION_API_URL: &str = "https://api.namecheap.com/xml.response";
const SANDBOX_API_URL: &str = "https://api.sandbox.namecheap.com/xml.response";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiError {
pub code: String,
pub message: String,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Namecheap API error {}: {}", self.code, self.message)
}
}
impl Error for ApiError {}
#[derive(Debug)]
pub enum NamecheapError {
Request(reqwest::Error),
Api(ApiError),
Parse(String),
DomainNotFound,
Unauthorized,
}
impl fmt::Display for NamecheapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NamecheapError::Request(e) => write!(f, "HTTP request error: {}", e),
NamecheapError::Api(e) => write!(f, "{}", e),
NamecheapError::Parse(msg) => write!(f, "XML parse error: {}", msg),
NamecheapError::DomainNotFound => write!(f, "Domain not found"),
NamecheapError::Unauthorized => write!(f, "Unauthorized"),
}
}
}
impl Error for NamecheapError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
NamecheapError::Request(e) => Some(e),
NamecheapError::Api(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for NamecheapError {
fn from(err: reqwest::Error) -> Self {
NamecheapError::Request(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostRecord {
pub host_id: String,
pub name: String,
pub record_type: String,
pub address: String,
pub mx_pref: Option<u16>,
pub ttl: u64,
}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub api_user: String,
pub api_key: String,
pub username: String,
pub client_ip: String,
pub environment: Environment,
}
impl ClientConfig {
pub fn new(
api_user: impl Into<String>,
api_key: impl Into<String>,
client_ip: impl Into<String>,
environment: Environment,
) -> Self {
let api_user = api_user.into();
Self {
username: api_user.clone(),
api_user,
api_key: api_key.into(),
client_ip: client_ip.into(),
environment,
}
}
pub fn sandbox(
api_user: impl Into<String>,
api_key: impl Into<String>,
client_ip: impl Into<String>,
) -> Self {
Self::new(api_user, api_key, client_ip, Environment::Sandbox)
}
pub fn production(
api_user: impl Into<String>,
api_key: impl Into<String>,
client_ip: impl Into<String>,
) -> Self {
Self::new(api_user, api_key, client_ip, Environment::Production)
}
pub fn api_url(&self) -> &'static str {
match self.environment {
Environment::Production => PRODUCTION_API_URL,
Environment::Sandbox => SANDBOX_API_URL,
}
}
}
#[derive(Debug, Clone)]
pub struct Client {
http_client: HttpClient,
config: ClientConfig,
}
impl Client {
pub fn new(config: ClientConfig) -> Result<Self, Box<dyn Error + Send + Sync>> {
Self::with_http_config(config, HttpClientConfig::default())
}
pub fn with_http_config(
config: ClientConfig,
http_config: HttpClientConfig,
) -> Result<Self, Box<dyn Error + Send + Sync>> {
let mut builder = HttpClient::builder().user_agent("manydns-rs/1.0.0");
if let Some(timeout) = http_config.timeout {
builder = builder.timeout(timeout);
}
if let Some(addr) = http_config.local_address {
builder = builder.local_address(addr);
}
#[cfg(any(
target_os = "android",
target_os = "fuchsia",
target_os = "linux",
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos",
target_os = "illumos",
target_os = "solaris",
))]
if let Some(ref iface) = http_config.interface {
builder = builder.interface(iface);
}
let http_client = builder.build()?;
Ok(Self {
http_client,
config,
})
}
pub fn environment(&self) -> Environment {
self.config.environment
}
fn url_encode(s: &str) -> String {
let mut encoded = String::with_capacity(s.len() * 3);
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
encoded.push(byte as char);
}
_ => {
encoded.push('%');
encoded.push_str(&format!("{:02X}", byte));
}
}
}
encoded
}
fn build_query_string(params: &[(&str, &str)]) -> String {
params
.iter()
.map(|(k, v)| format!("{}={}", k, Self::url_encode(v)))
.collect::<Vec<_>>()
.join("&")
}
async fn request(
&self,
command: &str,
params: &[(&str, &str)],
) -> Result<String, NamecheapError> {
let mut query_params: Vec<(&str, &str)> = vec![
("ApiUser", &self.config.api_user),
("ApiKey", &self.config.api_key),
("UserName", &self.config.username),
("ClientIp", &self.config.client_ip),
("Command", command),
];
query_params.extend_from_slice(params);
let query_string = Self::build_query_string(&query_params);
let url = format!("{}?{}", self.config.api_url(), query_string);
let response = self.http_client.get(&url).send().await?;
let text = response.text().await?;
self.check_api_error(&text)?;
Ok(text)
}
fn check_api_error(&self, xml: &str) -> Result<(), NamecheapError> {
let mut reader = Reader::from_str(xml);
let mut in_error = false;
let mut error_code: Option<String> = None;
let mut error_message: Option<String> = None;
let mut is_error_status = false;
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
let name = e.local_name();
if name.as_ref() == b"ApiResponse" {
for attr in e.attributes().flatten() {
if attr.key.as_ref() == b"Status" && attr.value.as_ref() == b"ERROR" {
is_error_status = true;
}
}
} else if name.as_ref() == b"Error" {
in_error = true;
for attr in e.attributes().flatten() {
if attr.key.as_ref() == b"Number" {
error_code =
String::from_utf8_lossy(&attr.value).into_owned().into();
}
}
}
}
Ok(Event::Text(ref e)) if in_error => {
error_message = Some(String::from_utf8_lossy(e.as_ref()).into_owned());
}
Ok(Event::End(ref e)) if e.local_name().as_ref() == b"Error" => {
in_error = false;
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(NamecheapError::Parse(format!("XML parse error: {}", e)));
}
_ => {}
}
}
if is_error_status {
if let (Some(code), Some(msg)) = (error_code, error_message) {
match code.as_str() {
"1010102" | "1011102" | "1030408" => {
return Err(NamecheapError::Unauthorized);
}
"2019166" | "2016166" => {
return Err(NamecheapError::DomainNotFound);
}
_ => {}
}
return Err(NamecheapError::Api(ApiError { code, message: msg }));
}
return Err(NamecheapError::Parse(
"Failed to parse error response".to_string(),
));
}
Ok(())
}
pub async fn get_hosts(&self, sld: &str, tld: &str) -> Result<Vec<HostRecord>, NamecheapError> {
#[cfg(debug_assertions)]
eprintln!("DEBUG get_hosts: sld='{}', tld='{}'", sld, tld);
let xml = self
.request(
"namecheap.domains.dns.getHosts",
&[("SLD", sld), ("TLD", tld)],
)
.await?;
#[cfg(debug_assertions)]
eprintln!("DEBUG get_hosts response:\n{}", &xml);
if let Some(using_our_dns) =
get_element_attr(&xml, "DomainDNSGetHostsResult", "IsUsingOurDNS")?
{
if using_our_dns != "true" {
return Err(NamecheapError::Api(ApiError {
code: "2030288".to_string(),
message: "Domain is not using Namecheap DNS servers".to_string(),
}));
}
}
let records = parse_host_records(&xml)?;
#[cfg(debug_assertions)]
eprintln!("DEBUG get_hosts parsed {} records", records.len());
Ok(records)
}
pub async fn set_hosts(
&self,
sld: &str,
tld: &str,
records: &[HostRecord],
) -> Result<(), NamecheapError> {
#[cfg(debug_assertions)]
eprintln!(
"DEBUG set_hosts: sld='{}', tld='{}', {} records",
sld,
tld,
records.len()
);
let mut params: Vec<(String, String)> = vec![
("SLD".to_string(), sld.to_string()),
("TLD".to_string(), tld.to_string()),
];
for (i, record) in records.iter().enumerate() {
let n = i + 1;
#[cfg(debug_assertions)]
eprintln!(
"DEBUG set_hosts record {}: name='{}', type='{}', address='{}', mx_pref={:?}",
n, record.name, record.record_type, record.address, record.mx_pref
);
params.push((format!("HostName{}", n), record.name.clone()));
params.push((format!("RecordType{}", n), record.record_type.clone()));
params.push((format!("Address{}", n), record.address.clone()));
params.push((format!("TTL{}", n), record.ttl.to_string()));
if let Some(mx_pref) = record.mx_pref {
params.push((format!("MXPref{}", n), mx_pref.to_string()));
}
}
let param_refs: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let response = self
.request("namecheap.domains.dns.setHosts", ¶m_refs)
.await?;
#[cfg(debug_assertions)]
eprintln!(
"DEBUG set_hosts response (first 1000 chars): {}",
&response[..response.len().min(1000)]
);
Ok(())
}
}
pub fn parse_host_records(xml: &str) -> Result<Vec<HostRecord>, NamecheapError> {
let mut reader = Reader::from_str(xml);
let mut records = Vec::new();
loop {
match reader.read_event() {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
let local_name = e.local_name();
if local_name.as_ref().eq_ignore_ascii_case(b"host") {
let mut host_id = String::new();
let mut name = String::new();
let mut record_type = String::new();
let mut address = String::new();
let mut ttl: u64 = 1800;
let mut mx_pref: Option<u16> = None;
for attr in e.attributes().flatten() {
let value = String::from_utf8_lossy(&attr.value).into_owned();
match attr.key.as_ref() {
b"HostId" => host_id = value,
b"Name" => name = value,
b"Type" => record_type = value,
b"Address" => address = value,
b"TTL" => ttl = value.parse().unwrap_or(1800),
b"MXPref" => mx_pref = value.parse().ok(),
_ => {}
}
}
records.push(HostRecord {
host_id,
name,
record_type,
address,
mx_pref,
ttl,
});
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(NamecheapError::Parse(format!("XML parse error: {}", e)));
}
_ => {}
}
}
Ok(records)
}
pub fn get_element_attr(
xml: &str,
tag: &str,
attr: &str,
) -> Result<Option<String>, NamecheapError> {
let mut reader = Reader::from_str(xml);
let tag_bytes = tag.as_bytes();
let attr_bytes = attr.as_bytes();
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
if e.local_name().as_ref() == tag_bytes {
for a in e.attributes().flatten() {
if a.key.as_ref() == attr_bytes {
return Ok(Some(String::from_utf8_lossy(&a.value).into_owned()));
}
}
return Ok(None);
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(NamecheapError::Parse(format!("XML parse error: {}", e)));
}
_ => {}
}
}
Ok(None)
}