From 3a7620b6d5b313578284a124511be63b50780be3 Mon Sep 17 00:00:00 2001 From: Kaan Genc Date: Sun, 23 Jan 2022 16:12:27 -0500 Subject: [PATCH] Use die-exit to clean up exits --- Cargo.lock | 6 +++++ Cargo.toml | 3 ++- Readme.md | 11 +++++++++ src/config.rs | 63 ++++++++++++++++++++++++++++++--------------------- src/main.rs | 61 +++++++++++++++++++++++++++++-------------------- 5 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 Readme.md diff --git a/Cargo.lock b/Cargo.lock index 92ad6d0..3ff67e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,11 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "die-exit" +version = "0.3.3" +source = "git+https://github.com/SeriousBug/die.git#76f91559d6b5c1c2f6a889b9cf9780c663918ac5" + [[package]] name = "directories" version = "4.0.1" @@ -255,6 +260,7 @@ name = "gandi-rust-dns-updater" version = "0.1.0" dependencies = [ "anyhow", + "die-exit", "directories", "futures", "json", diff --git a/Cargo.toml b/Cargo.toml index 27f1d6b..5a2c3d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,5 @@ directories = "4.0.1" structopt = "0.3.25" tokio = { version = "1.14.0", features = ["full"] } futures = "0.3.17" -anyhow = "1.0" \ No newline at end of file +anyhow = "1.0" +die-exit = { git = "https://github.com/SeriousBug/die.git" } \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..860f029 --- /dev/null +++ b/Readme.md @@ -0,0 +1,11 @@ +## gandi-live-dns-rust + +A program that can set the IP addresses for configured DNS entries in [Gandi](https://gandi.net)'s domain configuration. +Thanks to Gandi's [LiveDNS API](https://api.gandi.net/docs/livedns/), this creates a dynamic DNS system. + +Inspired by [cavebeat's similar tool](https://github.com/cavebeat/gandi-live-dns), +which seems to be unmaintained at the time I'm writing this. I decided to rewrite it in Rust as a learning project. + +## Usage + +- Copy `example.toml` to \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 5d09b32..ed5f8f8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,9 @@ +use crate::opts; +use anyhow; use directories::ProjectDirs; use serde::Deserialize; -use std::error::Error; use std::fs; use std::path::PathBuf; -use crate::opts; #[derive(Deserialize, Debug)] pub struct Entry { @@ -19,7 +19,7 @@ pub struct Config { pub entry: Vec, } -const DEFAULT_TYPES: Vec<&str> = vec!["A"]; +const DEFAULT_TYPES: &'static [&'static str] = &["A"]; impl Config { pub fn fqdn<'c>(entry: &'c Entry, config: &'c Config) -> &'c str { @@ -27,39 +27,50 @@ impl Config { } pub fn types<'e>(entry: &'e Entry) -> Vec<&'e str> { - return entry.types.as_ref().and_then( - |ts| Some(ts.iter().map(|t| t.as_str()).collect()) - ).unwrap_or(DEFAULT_TYPES); + return entry + .types + .as_ref() + .and_then(|ts| Some(ts.iter().map(|t| t.as_str()).collect())) + .unwrap_or_else(|| DEFAULT_TYPES.to_vec()); } } -pub fn load_config(file: PathBuf) -> Result> { - let output = fs::read_to_string(file)?; - let contents = output.as_str(); - - let config = toml::from_str(contents)?; - return Ok(config); +fn load_config_from>(path: P) -> anyhow::Result { + let contents = fs::read_to_string(path)?; + Ok(toml::from_str(&contents)?) } -pub fn validate_config(config: &Config) -> Result<(), String> { +pub fn load_config(opts: &opts::Opts) -> anyhow::Result { + match &opts.config { + Some(config_path) => load_config_from(&config_path), + None => { + let confpath = ProjectDirs::from("me", "kaangenc", "gandi-dynamic-dns") + .and_then(|dir| Some(PathBuf::from(dir.config_dir()).join("config.toml"))); + confpath.as_ref() + .and_then(|path| { + Some(load_config_from(path)) + }) + .unwrap_or_else(|| { + let path = PathBuf::from(".").join("gandi.toml"); + load_config_from(path) + }) + .or_else(|_| { + match &confpath { + Some(path) => anyhow::bail!("Can't find the config file! Please create {}, or gandi.toml in the current directory.", path.to_string_lossy()), + None => anyhow::bail!("Can't find the config file! Please create gandi.toml in the current directory."), + } + }) + } + } +} + +pub fn validate_config(config: &Config) -> anyhow::Result<()> { for entry in &config.entry { for entry_type in Config::types(&entry) { if entry_type != "A" && entry_type != "AAA" { - return Err(format!("Entry {} has invalid type {}", entry.name, entry_type)); + anyhow::bail!("Entry {} has invalid type {}", entry.name, entry_type); } } } return Ok(()); } - -pub fn config_path(opts: &opts::Opts) -> PathBuf { - return opts - .config - .as_ref() - .and_then(|conf| Some(PathBuf::from(conf))) - .unwrap_or( - ProjectDirs::from("me", "kaangenc", "gandi-dynamic-dns") - .and_then(|dir| Some(PathBuf::from(dir.config_dir()))) - .unwrap_or(PathBuf::from(".")), - ); -} diff --git a/src/main.rs b/src/main.rs index 510ff47..83c38fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,12 @@ use crate::config::Config; use anyhow; use futures; use reqwest::{header, Client, ClientBuilder, StatusCode}; -use std::collections::HashMap; +use std::{collections::HashMap, process::exit}; use structopt::StructOpt; use tokio::{self, task::JoinHandle}; mod config; mod opts; - -fn gandi_api_get(fqdn: &str) -> String { - return format!("https://api.gandi.net/v5/livedns/domains/{}/records", fqdn); -} +use die_exit::*; fn gandi_api_url(fqdn: &str, rrset_name: &str, rrset_type: &str) -> String { return format!( @@ -33,29 +30,47 @@ fn api_client(api_key: &str) -> anyhow::Result { return Ok(client); } +async fn get_ip(api_url: &str) -> anyhow::Result { + let response = reqwest::get(api_url).await?; + let text = response.text().await?; + Ok(text) +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let opts = opts::Opts::from_args(); - let conf_path = config::config_path(&opts); - println!("Loading config from {:#?}", conf_path); - let conf = config::load_config(conf_path)?; - config::validate_config(&conf)?; + let conf = config::load_config(&opts) + .die_with(|error| format!("Failed to read config file: {}", error)); + config::validate_config(&conf).die_with(|error| format!("Invalid config: {}", error)); + println!("Finding out the API address..."); + let ipv4 = get_ip("https://api.ipify.org").await; + let ipv6 = get_ip("https://api6.ipify.org").await; + println!("Found these:"); + println!("\tIPv4: {}", ipv4.unwrap_or_else(|error| error.to_string())); + println!("\tIPv6: {}", ipv6.unwrap_or_else(|error| error.to_string())); let client = api_client(&conf.api_key)?; - - let ipv4 = String::from("173.89.215.91"); - let ipv6 = String::from("2603:6011:be07:302:79f4:50dd:6abe:be38"); - - let mut results: Vec> = Vec::new(); + let mut tasks: Vec> = Vec::new(); + println!("Attempting to update DNS entries now"); for entry in &conf.entry { for entry_type in Config::types(entry) { let fqdn = Config::fqdn(&entry, &conf); let url = gandi_api_url(fqdn, entry.name.as_str(), entry_type); - let ip = if entry_type.eq("A") { - ipv4.as_str() - } else { - ipv6.as_str() + let ip = match entry_type { + "A" => ipv4.unwrap_or_else(|error| { + panic!( + "Need IPv4 address for {} but failed to get it: {}", + fqdn, error + ) + }), + "AAA" => ipv6.unwrap_or_else(|error| { + panic!( + "Need IPv6 address for {} but failed to get it: {}", + fqdn, error + ) + }), + &_ => panic!("Unexpected entry type {}", entry_type), }; let mut map = HashMap::new(); map.insert("rrset_values", ip); @@ -69,17 +84,15 @@ async fn main() -> anyhow::Result<()> { .await .unwrap_or_else(|error| error.to_string()), ), - Err(error) => ( - StatusCode::IM_A_TEAPOT, error.to_string() - ), + Err(error) => (StatusCode::IM_A_TEAPOT, error.to_string()), } }); - results.push(task); + tasks.push(task); } } - let results = futures::future::try_join_all(results).await?; - + let results = futures::future::try_join_all(tasks).await?; + println!("Updates done for {} entries", results.len()); for (status, body) in results { println!("{} - {}", status, body); }