diff --git a/binaries/linux-aarch64/dreamhost-ddns b/binaries/linux-aarch64/dreamhost-ddns index 2fc1b5c..6c715b0 100644 Binary files a/binaries/linux-aarch64/dreamhost-ddns and b/binaries/linux-aarch64/dreamhost-ddns differ diff --git a/binaries/linux-rpi-armv7/dreamhost-ddns b/binaries/linux-rpi-armv7/dreamhost-ddns index 14fa241..44c3db7 100644 Binary files a/binaries/linux-rpi-armv7/dreamhost-ddns and b/binaries/linux-rpi-armv7/dreamhost-ddns differ diff --git a/binaries/linux-x86_64/dreamhost-ddns b/binaries/linux-x86_64/dreamhost-ddns index f7b48ca..7cabe19 100644 Binary files a/binaries/linux-x86_64/dreamhost-ddns and b/binaries/linux-x86_64/dreamhost-ddns differ diff --git a/binaries/windows/dreamhost-ddns.exe b/binaries/windows/dreamhost-ddns.exe index 325f42c..2eb6690 100644 Binary files a/binaries/windows/dreamhost-ddns.exe and b/binaries/windows/dreamhost-ddns.exe differ diff --git a/src/main.rs b/src/main.rs index cd44793..8744cce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,42 +7,40 @@ use std::net::IpAddr; use std::sync::mpsc; use std::thread; - #[derive(Parser)] #[command( name = "dreamhost-ddns", version, - about = "Updates a DreamHost DNS A record with the current WAN IP", - long_about = None + about = "Updates a DreamHost DNS A record with the current WAN IP" )] struct Args { #[arg(short, long)] verbose: bool, - #[arg(short, long, default_value = "config.toml")] - config: String, - - #[arg(long)] - dry_run: bool, + #[arg(short, long)] + config: Option, #[arg(long)] api_key: Option, #[arg(long)] record: Option, + + #[arg(long, default_value_t = 300)] + interval: u64, + + #[arg(long)] + dry_run: bool, } #[derive(Debug, Deserialize)] struct Record { record: String, + #[serde(rename = "type")] record_type: String, - value: String, -} -#[derive(Debug, Deserialize)] -struct ApiResponse { - data: Option>, + value: String, } #[derive(Debug, Deserialize)] @@ -51,6 +49,73 @@ struct Config { dns_record: String, } +struct DreamhostClient { + client: Client, + api_key: String, +} + +impl DreamhostClient { + fn call(&self, params: &[(&str, &str)]) -> Result { + let mut query = vec![ + ("key", self.api_key.as_str()), + ("format", "json"), + ]; + + query.extend_from_slice(params); + + let resp: serde_json::Value = self.client + .get("https://api.dreamhost.com/") + .query(&query) + .send()? + .json()?; + + if resp["result"] != "success" { + let reason = resp["reason"] + .as_str() + .unwrap_or("Unknown DreamHost API error"); + + return Err(anyhow!("DreamHost API error: {}", reason)); + } + + Ok(resp) + } + + fn get_dns_ip(&self, record_name: &str) -> Result { + let resp = self.call(&[ + ("cmd", "dns-list_records"), + ])?; + + let records: Vec = serde_json::from_value(resp["data"].clone())?; + + records + .into_iter() + .find(|r| r.record == record_name && r.record_type == "A") + .map(|r| r.value) + .ok_or_else(|| anyhow!("DreamHost error: DNS record '{}' not found", record_name)) + } + + fn update_dns(&self, record: &str, old_ip: &str, new_ip: &str) -> Result<()> { + info!("Adding new DNS record {} -> {}", record, new_ip); + + self.call(&[ + ("cmd", "dns-add_record"), + ("record", record), + ("type", "A"), + ("value", new_ip), + ])?; + + info!("Removing old DNS record {} -> {}", record, old_ip); + + self.call(&[ + ("cmd", "dns-remove_record"), + ("record", record), + ("type", "A"), + ("value", old_ip), + ])?; + + Ok(()) + } +} fn main() -> Result<()> { @@ -64,26 +129,35 @@ fn main() -> Result<()> { env_logger::init(); } - let config = load_config(&args.config)?; + let config = resolve_config(&args)?; + let api_key = args.api_key.unwrap_or(config.dreamhost_api_key); let record = args.record.unwrap_or(config.dns_record); + info!("Record: {}", record); + info!("Check interval: {} seconds", args.interval); + let client = Client::builder() - .timeout(std::time::Duration::from_secs(3)) + .timeout(std::time::Duration::from_secs(5)) .user_agent("dreamhost-ddns/1.0") .build()?; - let wan_ip = get_wan_ip(&client)?; + let dh = DreamhostClient { + client, + api_key, + }; + + let wan_ip = get_wan_ip(&dh.client)?; info!("Detected WAN IP: {}", wan_ip); - let dns_ip = get_dns_ip(&client, &api_key, &record)?; + let dns_ip = dh.get_dns_ip(&record)?; info!("DNS record IP: {}", dns_ip); if wan_ip.to_string() == dns_ip { info!("DNS already up-to-date"); return Ok(()); } - + warn!("IP mismatch detected."); if args.dry_run { @@ -93,14 +167,62 @@ fn main() -> Result<()> { ); } else { info!("Updating DNS..."); - update_dns(&client, &api_key, &record, &dns_ip, &wan_ip.to_string())?; + dh.update_dns(&record, &dns_ip, &wan_ip.to_string())?; + info!("DNS updated successfully"); } - info!("DNS updated successfully"); Ok(()) } +fn resolve_config(args: &Args) -> Result { + + let mut api_key = args.api_key.clone(); + let mut record = args.record.clone(); + + // Environment variables + if api_key.is_none() { + api_key = std::env::var("DREAMHOST_API_KEY").ok(); + } + + if record.is_none() { + record = std::env::var("DNS_RECORD").ok(); + } + + // Explicit config file + if (api_key.is_none() || record.is_none()) && args.config.is_some() { + let cfg = load_config(args.config.as_ref().unwrap())?; + + if api_key.is_none() { + api_key = Some(cfg.dreamhost_api_key); + } + + if record.is_none() { + record = Some(cfg.dns_record); + } + } + + // Default config.toml + if (api_key.is_none() || record.is_none()) && std::path::Path::new("config.toml").exists() { + let cfg = load_config("config.toml")?; + + if api_key.is_none() { + api_key = Some(cfg.dreamhost_api_key); + } + + if record.is_none() { + record = Some(cfg.dns_record); + } + } + + let api_key = api_key.ok_or_else(|| anyhow!("Missing DreamHost API key"))?; + let record = record.ok_or_else(|| anyhow!("Missing DNS record"))?; + + Ok(Config { + dreamhost_api_key: api_key, + dns_record: record, + }) +} fn load_config(path: &str) -> Result { let contents = std::fs::read_to_string(path)?; @@ -108,7 +230,6 @@ fn load_config(path: &str) -> Result { Ok(config) } - fn get_wan_ip(client: &Client) -> Result { let services = [ "https://icanhazip.com", @@ -136,73 +257,13 @@ fn get_wan_ip(client: &Client) -> Result { }); } - drop(tx); // close channel when threads finish + drop(tx); match rx.recv() { Ok((url, ip)) => { info!("WAN IP detected via {}: {}", url, ip); Ok(ip) } - Err(_) => Err(anyhow!("Could not determine WAN IP")), + Err(_) => Err(anyhow!("All WAN IP detection services failed")), } -} - -fn get_dns_ip(client: &Client, api_key: &str, record_name: &str) -> Result { - - let res: ApiResponse = client - .get("https://api.dreamhost.com/") - .query(&[ - ("key", api_key), - ("cmd", "dns-list_records"), - ("format", "json"), - ]) - .send()? - .json()?; - - let records = res.data.ok_or_else(|| anyhow!("No DNS data returned"))?; - - records - .into_iter() - .find(|r| r.record == record_name && r.record_type == "A") - .map(|r| r.value) - .ok_or_else(|| anyhow!("DNS record not found")) -} - -fn update_dns(client: &Client, api_key: &str, record: &str, old_ip: &str, new_ip: &str) -> Result<()> { - - info!("Adding new DNS record {} -> {}", record, new_ip); - - client - .get("https://api.dreamhost.com/") - .query(&[ - ("key", api_key), - ("cmd", "dns-add_record"), - ("record", record), - ("type", "A"), - ("value", new_ip), - ("format", "json"), - ]) - .send()? - .error_for_status()?; - - info!("New record added successfully"); - - info!("Removing old DNS record {} -> {}", record, old_ip); - - client - .get("https://api.dreamhost.com/") - .query(&[ - ("key", api_key), - ("cmd", "dns-remove_record"), - ("record", record), - ("type", "A"), - ("value", old_ip), - ("format", "json"), - ]) - .send()? - .error_for_status()?; - - info!("Old record removed"); - - Ok(()) } \ No newline at end of file