Use die-exit to clean up exits

This commit is contained in:
Kaan Genc 2022-01-23 16:12:27 -05:00
parent c5ed8fe6ab
commit 3a7620b6d5
5 changed files with 93 additions and 51 deletions

6
Cargo.lock generated
View file

@ -101,6 +101,11 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "die-exit"
version = "0.3.3"
source = "git+https://github.com/SeriousBug/die.git#76f91559d6b5c1c2f6a889b9cf9780c663918ac5"
[[package]] [[package]]
name = "directories" name = "directories"
version = "4.0.1" version = "4.0.1"
@ -255,6 +260,7 @@ name = "gandi-rust-dns-updater"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"die-exit",
"directories", "directories",
"futures", "futures",
"json", "json",

View file

@ -16,3 +16,4 @@ structopt = "0.3.25"
tokio = { version = "1.14.0", features = ["full"] } tokio = { version = "1.14.0", features = ["full"] }
futures = "0.3.17" futures = "0.3.17"
anyhow = "1.0" anyhow = "1.0"
die-exit = { git = "https://github.com/SeriousBug/die.git" }

11
Readme.md Normal file
View file

@ -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

View file

@ -1,9 +1,9 @@
use crate::opts;
use anyhow;
use directories::ProjectDirs; use directories::ProjectDirs;
use serde::Deserialize; use serde::Deserialize;
use std::error::Error;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use crate::opts;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Entry { pub struct Entry {
@ -19,7 +19,7 @@ pub struct Config {
pub entry: Vec<Entry>, pub entry: Vec<Entry>,
} }
const DEFAULT_TYPES: Vec<&str> = vec!["A"]; const DEFAULT_TYPES: &'static [&'static str] = &["A"];
impl Config { impl Config {
pub fn fqdn<'c>(entry: &'c Entry, config: &'c Config) -> &'c str { 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> { pub fn types<'e>(entry: &'e Entry) -> Vec<&'e str> {
return entry.types.as_ref().and_then( return entry
|ts| Some(ts.iter().map(|t| t.as_str()).collect()) .types
).unwrap_or(DEFAULT_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<Config, Box<dyn Error>> { fn load_config_from<P: std::convert::AsRef<std::path::Path>>(path: P) -> anyhow::Result<Config> {
let output = fs::read_to_string(file)?; let contents = fs::read_to_string(path)?;
let contents = output.as_str(); Ok(toml::from_str(&contents)?)
let config = toml::from_str(contents)?;
return Ok(config);
} }
pub fn validate_config(config: &Config) -> Result<(), String> { pub fn load_config(opts: &opts::Opts) -> anyhow::Result<Config> {
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 in &config.entry {
for entry_type in Config::types(&entry) { for entry_type in Config::types(&entry) {
if entry_type != "A" && entry_type != "AAA" { 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(()); 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(".")),
);
}

View file

@ -2,15 +2,12 @@ use crate::config::Config;
use anyhow; use anyhow;
use futures; use futures;
use reqwest::{header, Client, ClientBuilder, StatusCode}; use reqwest::{header, Client, ClientBuilder, StatusCode};
use std::collections::HashMap; use std::{collections::HashMap, process::exit};
use structopt::StructOpt; use structopt::StructOpt;
use tokio::{self, task::JoinHandle}; use tokio::{self, task::JoinHandle};
mod config; mod config;
mod opts; mod opts;
use die_exit::*;
fn gandi_api_get(fqdn: &str) -> String {
return format!("https://api.gandi.net/v5/livedns/domains/{}/records", fqdn);
}
fn gandi_api_url(fqdn: &str, rrset_name: &str, rrset_type: &str) -> String { fn gandi_api_url(fqdn: &str, rrset_name: &str, rrset_type: &str) -> String {
return format!( return format!(
@ -33,29 +30,47 @@ fn api_client(api_key: &str) -> anyhow::Result<Client> {
return Ok(client); return Ok(client);
} }
async fn get_ip(api_url: &str) -> anyhow::Result<String> {
let response = reqwest::get(api_url).await?;
let text = response.text().await?;
Ok(text)
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let opts = opts::Opts::from_args(); let opts = opts::Opts::from_args();
let conf_path = config::config_path(&opts); let conf = config::load_config(&opts)
println!("Loading config from {:#?}", conf_path); .die_with(|error| format!("Failed to read config file: {}", error));
let conf = config::load_config(conf_path)?; config::validate_config(&conf).die_with(|error| format!("Invalid config: {}", error));
config::validate_config(&conf)?; 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 client = api_client(&conf.api_key)?;
let mut tasks: Vec<JoinHandle<(StatusCode, String)>> = Vec::new();
let ipv4 = String::from("173.89.215.91"); println!("Attempting to update DNS entries now");
let ipv6 = String::from("2603:6011:be07:302:79f4:50dd:6abe:be38");
let mut results: Vec<JoinHandle<(StatusCode, String)>> = Vec::new();
for entry in &conf.entry { for entry in &conf.entry {
for entry_type in Config::types(entry) { for entry_type in Config::types(entry) {
let fqdn = Config::fqdn(&entry, &conf); let fqdn = Config::fqdn(&entry, &conf);
let url = gandi_api_url(fqdn, entry.name.as_str(), entry_type); let url = gandi_api_url(fqdn, entry.name.as_str(), entry_type);
let ip = if entry_type.eq("A") { let ip = match entry_type {
ipv4.as_str() "A" => ipv4.unwrap_or_else(|error| {
} else { panic!(
ipv6.as_str() "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(); let mut map = HashMap::new();
map.insert("rrset_values", ip); map.insert("rrset_values", ip);
@ -69,17 +84,15 @@ async fn main() -> anyhow::Result<()> {
.await .await
.unwrap_or_else(|error| error.to_string()), .unwrap_or_else(|error| error.to_string()),
), ),
Err(error) => ( Err(error) => (StatusCode::IM_A_TEAPOT, error.to_string()),
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 { for (status, body) in results {
println!("{} - {}", status, body); println!("{} - {}", status, body);
} }