Skip to content
This repository has been archived by the owner on Jun 13, 2022. It is now read-only.

Commit

Permalink
add config file and multi account support
Browse files Browse the repository at this point in the history
  • Loading branch information
ciphax committed Feb 28, 2019
1 parent a0e65e6 commit 8cfe5e2
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 281 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ reqwest = "0.9.10"
trust-dns = "0.15.1"
clap = "2.32.0"
openssl-probe = "0.1.2"

[dependencies.cookie]
version = "0.11.0"
features = ["percent-encode"]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
cookie = { version = "0.11.0", features = ["percent-encode"] }
189 changes: 189 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use std::fs::File;
use std::io::BufReader;
use std::thread::sleep;
use std::time::{Duration, Instant};
use clap::{Arg, App, SubCommand};
use config::Config;
use inwx::{Inwx, InwxError};
use dns::{check_txt_record, lookup_real_domain};

impl From<InwxError> for String {
fn from(inwx_error: InwxError) -> String {
format!("{}", inwx_error)
}
}

fn execute_api_commands<F>(config: &Config, domain: &str, op: F) -> Result<bool, String> where F: Fn(&mut Inwx) -> Result<(), InwxError> {
if config.accounts.len() == 0 {
return Err("No accounts configured".to_owned());
}

let accounts = match(&config.accounts).into_iter().find(|account|
(&account.domains).into_iter().any(|d| domain == d || domain.ends_with(&format!(".{}", d)))
) {
Some(account) => vec!(account.clone()),
None => config.accounts.clone()
};


for account in accounts {
let mut success = false;
let mut api = Inwx::new(&account)?;

let mut err = None;
match op(&mut api) {
Err(InwxError::DomainNotFound) => {},
Err(e) => {
err = Some(e);
},
_ => {
success = true;
}
}

if let Err(e) = api.logout() {
if let None = err {
err = Some(e);
}
}

if let Some(e) = err {
return Err(String::from(e));
} else if success {
return Ok(account.ote);
}
}

Err(String::from(InwxError::DomainNotFound))
}

fn read_config(path: &str) -> Result<Config, String> {
let file = File::open(path).map_err(|e| format!("Failed to open config file: {}", e))?;
let reader = BufReader::new(file);
Ok(serde_json::from_reader(reader).map_err(|e| format!("Failed to parse config file: {}", e))?)
}

fn create(config: &Config, domain: &str, value: &str) -> Result<(), String> {
println!("Creating TXT record...");

let is_ote = execute_api_commands(&config, &domain, |api| {
api.create_txt_record(&domain, &value)?;
Ok(())
})?;

println!("=> done!");

if !is_ote && !config.options.no_dns_check {
println!("Waiting for the dns record to be publicly visible...");

let start = Instant::now();
let mut wait_secs = 5;

loop {
// timeout after 10 minutes
if start.elapsed() > Duration::from_secs(60 * 10) {
return Err("timeout!".to_owned());
}

if check_txt_record(&config.options.dns_server, &domain, value) {
break;
}

wait_secs *= 2;

sleep(Duration::from_secs(wait_secs));
}

println!("=> done!");
}

if config.options.wait_interval > 0 {
println!("Waiting {} additional seconds...", &config.options.wait_interval);

sleep(Duration::from_secs(config.options.wait_interval));

println!("=> done!");
}

Ok(())
}

fn delete(config: &Config, domain: &str) -> Result<(), String> {
println!("Deleting TXT record...");

execute_api_commands(&config, &domain, |api| {
api.delete_txt_record(&domain)?;
Ok(())
})?;

println!("=> done!");

Ok(())
}

pub fn run() -> Result<(), String> {
let mut app = App::new("letsencrypt-inwx")
.version("1.1.2")
.about("A small cli utility for automating the letsencrypt dns-01 challenge for domains hosted by inwx")
.subcommand(SubCommand::with_name("create")
.about("create a TXT record")
.arg(Arg::with_name("configfile")
.short("c")
.value_name("CONFIG_FILE")
.help("specify the path to the configfile")
.takes_value(true)
.required(true)
)
.arg(Arg::with_name("domain")
.short("d")
.value_name("DOMAIN")
.help("the domain of the record (i.e. \"_acme-challenge.example.com\"")
.takes_value(true)
.required(true)
)
.arg(Arg::with_name("value")
.short("v")
.value_name("VALUE")
.help("the value of the record")
.takes_value(true)
.required(true)
)
)
.subcommand(SubCommand::with_name("delete")
.about("delete a TXT record")
.arg(Arg::with_name("configfile")
.short("c")
.value_name("CONFIG_FILE")
.help("specify the path to the configfile")
.takes_value(true)
.required(true)
)
.arg(Arg::with_name("domain")
.short("d")
.value_name("DOMAIN")
.help("the domain of the record (i.e. \"_acme-challenge.example.com\"")
.takes_value(true)
.required(true)
)
);

let matches = app.clone().get_matches();

if let Some(matches) = matches.subcommand_matches("create") {
let config = read_config(matches.value_of("configfile").unwrap())?;
let domain = lookup_real_domain(&config.options.dns_server, matches.value_of("domain").unwrap());
let value = matches.value_of("value").unwrap();

create(&config, &domain, &value)?;
} else if let Some(matches) = matches.subcommand_matches("delete") {
let config = read_config(matches.value_of("configfile").unwrap())?;
let domain = lookup_real_domain(&config.options.dns_server, matches.value_of("domain").unwrap());

delete(&config, &domain)?;
} else {
app.print_help().unwrap();
std::process::exit(1);
}

Ok(())
}
45 changes: 45 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use serde::Deserialize;

#[derive(Deserialize, Debug, Clone)]
#[serde(default)]
pub struct Config {
pub accounts: Vec<Account>,
pub options: Options
}

impl Default for Config {
fn default() -> Config {
Config {
accounts: vec!(),
options: Options::default()
}
}
}

#[derive(Deserialize, Debug, Clone)]
pub struct Account {
pub username: String,
pub password: String,
#[serde(default)]
pub domains: Vec<String>,
#[serde(default)]
pub ote: bool
}

#[derive(Deserialize, Debug, Clone)]
#[serde(default)]
pub struct Options {
pub no_dns_check: bool,
pub wait_interval: u64,
pub dns_server: String
}

impl Default for Options {
fn default() -> Options {
Options {
no_dns_check: false,
wait_interval: 5,
dns_server: "8.8.8.8".to_owned()
}
}
}
18 changes: 9 additions & 9 deletions src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use trust_dns::udp::UdpClientConnection;
use trust_dns::op::DnsResponse;
use trust_dns::rr::{DNSClass, Name, RData, Record, RecordType};

fn dns_client() -> SyncClient<UdpClientConnection> {
let address = "8.8.8.8:53".parse().unwrap();
fn dns_client(dns_server: &str) -> SyncClient<UdpClientConnection> {
let address = format!("{}:53", dns_server).parse().unwrap();
let conn = UdpClientConnection::new(address).unwrap();
SyncClient::new(conn)
}
Expand All @@ -30,8 +30,8 @@ pub fn remove_trailing_dot(domain: &str) -> String {
domain
}

fn check_cname(domain: &str) -> Option<String> {
let client = dns_client();
fn check_cname(dns_server: &str, domain: &str) -> Option<String> {
let client = dns_client(dns_server);
let name = Name::from_str(&add_trailing_dot(domain)).ok()?;
let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::CNAME).ok()?;
let answers: &[Record] = response.answers();
Expand All @@ -45,11 +45,11 @@ fn check_cname(domain: &str) -> Option<String> {
None
}

pub fn lookup_real_domain(domain: &str) -> String {
pub fn lookup_real_domain(dns_server: &str, domain: &str) -> String {
let mut depth = 0;

let mut domain = domain.to_owned();
while let Some(real_name) = check_cname(&domain) {
while let Some(real_name) = check_cname(dns_server, &domain) {
domain = real_name;

if depth >= 10 {
Expand All @@ -62,8 +62,8 @@ pub fn lookup_real_domain(domain: &str) -> String {
domain
}

pub fn check_txt_record(domain: &str, value: &str) -> bool {
let client = dns_client();
pub fn check_txt_record(dns_server: &str, domain: &str, value: &str) -> bool {
let client = dns_client(dns_server);
let name = match Name::from_str(&add_trailing_dot(domain)) {
Ok(name) => name,
Err(_) => return false
Expand All @@ -86,4 +86,4 @@ pub fn check_txt_record(domain: &str, value: &str) -> bool {
}

false
}
}
Loading

0 comments on commit 8cfe5e2

Please sign in to comment.