From 8cfe5e2668214db0e5389386cb5c4f37ecdc3200 Mon Sep 17 00:00:00 2001 From: Matthias Herzog <37505324+kegato@users.noreply.github.com> Date: Fri, 1 Mar 2019 00:18:51 +0100 Subject: [PATCH] add config file and multi account support --- Cargo.lock | 16 ++++ Cargo.toml | 7 +- src/cli.rs | 189 +++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 45 ++++++++++++ src/dns.rs | 18 ++--- src/inwx.rs | 90 +++++++++-------------- src/lib.rs | 9 --- src/main.rs | 198 ++++---------------------------------------------- src/rpc.rs | 57 ++++++++++----- 9 files changed, 348 insertions(+), 281 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/config.rs delete mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index be94776..e709500 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -464,6 +464,8 @@ dependencies = [ "cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", "sxd-document 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "sxd-xpath 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "trust-dns 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1019,6 +1021,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "serde" version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "serde_json" @@ -1649,6 +1664,7 @@ dependencies = [ "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850" +"checksum serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "beed18e6f5175aef3ba670e57c60ef3b1b74d250d962a26604bff4c80e970dd4" "checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9" "checksum serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d48f9f99cd749a2de71d29da5f948de7f2764cc5a9d7f3c97e3514d4ee6eabf2" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" diff --git a/Cargo.toml b/Cargo.toml index 6831173..9f7894a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..ea08882 --- /dev/null +++ b/src/cli.rs @@ -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 for String { + fn from(inwx_error: InwxError) -> String { + format!("{}", inwx_error) + } +} + +fn execute_api_commands(config: &Config, domain: &str, op: F) -> Result 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 { + 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(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6611085 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,45 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +#[serde(default)] +pub struct Config { + pub accounts: Vec, + 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, + #[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() + } + } +} diff --git a/src/dns.rs b/src/dns.rs index f33f86c..0fea8c4 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -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 { - let address = "8.8.8.8:53".parse().unwrap(); +fn dns_client(dns_server: &str) -> SyncClient { + let address = format!("{}:53", dns_server).parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); SyncClient::new(conn) } @@ -30,8 +30,8 @@ pub fn remove_trailing_dot(domain: &str) -> String { domain } -fn check_cname(domain: &str) -> Option { - let client = dns_client(); +fn check_cname(dns_server: &str, domain: &str) -> Option { + 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(); @@ -45,11 +45,11 @@ fn check_cname(domain: &str) -> Option { 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 { @@ -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 @@ -86,4 +86,4 @@ pub fn check_txt_record(domain: &str, value: &str) -> bool { } false -} \ No newline at end of file +} diff --git a/src/inwx.rs b/src/inwx.rs index d72669c..f696f1e 100644 --- a/src/inwx.rs +++ b/src/inwx.rs @@ -2,74 +2,59 @@ use std::fmt; use cookie::CookieJar; use sxd_xpath::{evaluate_xpath, Value}; use super::rpc::{RpcRequest, RpcResponse, RpcRequestParameter, RpcRequestParameterValue, RpcError}; +use super::config::Account; const API_URL: &str = "https://api.domrobot.com/xmlrpc/"; +const OTE_API_URL: &str = "https://api.ote.domrobot.com/xmlrpc/"; #[derive(Debug)] pub enum InwxError { RpcError(RpcError), - ApiError { - method: String, - reason: String, - msg: String - } + DomainNotFound, + RecordNotFound } impl fmt::Display for InwxError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - &InwxError::RpcError(ref e) => write!(f, "The inwx api call failed: {}", e), - &InwxError::ApiError { ref method, ref msg, ref reason } => write!(f, "method={},msg={},reason={}", method, msg, reason) + &InwxError::RpcError(ref e) => write!(f, "An inwx api call failed: {}", e), + &InwxError::DomainNotFound => write!(f, "There is no nameserver for the specified domain"), + &InwxError::RecordNotFound => write!(f, "The specified record does not exist") } } } -pub struct Inwx { - cookies: CookieJar +impl From for InwxError { + fn from(rpc_error: RpcError) -> InwxError { + InwxError::RpcError(rpc_error) + } } -impl Inwx { - fn send_request(&mut self, request: RpcRequest) -> Result { - let method = request.get_method(); - - let response = request.send(API_URL, &mut self.cookies).map_err(|e| InwxError::RpcError(e))?; +pub struct Inwx<'a> { + cookies: CookieJar, + account: &'a Account +} - if response.is_success() { - Ok(response) - } else { - let msg = match evaluate_xpath( - &response.get_document(), - "/methodResponse/params/param/value/struct/member[name/text()=\"msg\"]/value/string/text()" - ) { - Ok(ref value) => value.string(), - Err(_) => String::new() - }; +impl<'a> Inwx<'a> { + fn send_request(&mut self, request: RpcRequest) -> Result { + let url = match self.account.ote { + true => OTE_API_URL, + false => API_URL + }; + let response = request.send(url, &mut self.cookies)?; - let reason = match evaluate_xpath( - &response.get_document(), - "/methodResponse/params/param/value/struct/member[name/text()=\"reason\"]/value/string/text()" - ) { - Ok(ref value) => value.string(), - Err(_) => String::new() - }; - - Err(InwxError::ApiError { - msg, - reason, - method - }) - } + Ok(response) } - fn login(&mut self, user: &str, pass: &str) -> Result<(), InwxError> { + fn login(&mut self) -> Result<(), InwxError> { let request = RpcRequest::new("account.login", &[ RpcRequestParameter { name: "user", - value: RpcRequestParameterValue::String(user.to_owned()) + value: RpcRequestParameterValue::String(self.account.username.to_owned()) }, RpcRequestParameter { name: "pass", - value: RpcRequestParameterValue::String(pass.to_owned()) + value: RpcRequestParameterValue::String(self.account.password.to_owned()) } ]); @@ -78,12 +63,13 @@ impl Inwx { Ok(()) } - pub fn new(user: &str, pass: &str) -> Result { + pub fn new(account: &'a Account) -> Result, InwxError> { let mut api = Inwx { - cookies: CookieJar::new() + cookies: CookieJar::new(), + account }; - api.login(user, pass)?; + api.login()?; Ok(api) } @@ -92,12 +78,6 @@ impl Inwx { let page_size = 20; let mut page = 1; - let dnf_error = || InwxError::ApiError { - method: "nameserver.list".to_owned(), - msg: "Domain not found".to_owned(), - reason: "".to_owned() - }; - loop { let request = RpcRequest::new("nameserver.list", &[ RpcRequestParameter { @@ -118,7 +98,7 @@ impl Inwx { ) .ok() .and_then(|value| value.string().parse().ok()) - .ok_or_else(dnf_error)?; + .ok_or_else(|| InwxError::DomainNotFound)?; if let Ok(Value::Nodeset(ref nodes)) = evaluate_xpath(&response.get_document(), "/methodResponse/params/param/value/struct/member[name/text()=\"resData\"]/value/struct/member[name/text()=\"domains\"]/value/array/data/value/struct/member[name/text()=\"domain\"]/value/string/text()") { for node in nodes { @@ -139,7 +119,7 @@ impl Inwx { if total > page * page_size { page += 1; } else { - return Err(dnf_error()); + return Err(InwxError::DomainNotFound); } } } @@ -196,11 +176,7 @@ impl Inwx { Err(_) => None }; - id.ok_or(InwxError::ApiError { - method: "nameserver.info".to_owned(), - msg: "Record not found".to_owned(), - reason: "".to_owned() - }) + id.ok_or_else(|| InwxError::RecordNotFound) } pub fn delete_txt_record(&mut self, domain: &str) -> Result<(), InwxError> { diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 107f0e0..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -extern crate cookie; -extern crate reqwest; -extern crate sxd_document; -extern crate sxd_xpath; -extern crate trust_dns; - -mod rpc; -pub mod inwx; -pub mod dns; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index de2ea28..d2c51dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,195 +1,25 @@ extern crate clap; extern crate openssl_probe; -extern crate letsencrypt_inwx; +extern crate serde; +extern crate serde_json; +extern crate cookie; +extern crate reqwest; +extern crate sxd_document; +extern crate sxd_xpath; +extern crate trust_dns; + +mod config; +mod rpc; +mod inwx; +mod dns; +mod cli; -use clap::{Arg, App, SubCommand}; -use std::fs::File; use std::process::exit; -use std::io::prelude::*; -use std::thread::sleep; -use std::time::{Duration, Instant}; -use letsencrypt_inwx::inwx::{Inwx, InwxError}; -use letsencrypt_inwx::dns::{check_txt_record, lookup_real_domain}; - -fn read_file(path: &str) -> std::io::Result { - let mut file = File::open(path)?; - let mut content = String::new(); - file.read_to_string(&mut content)?; - - Ok(content) -} - -fn read_credentials(path: &str) -> Result<(String, String), &'static str> { - let content = match read_file(path) { - Ok(content) => content, - Err(_) => return Err("Could not read the credential file!") - }; - - let content = content.replace("\r\n", "\n").replace("\r", "\n"); - let mut lines = content.split("\n"); - - if let Some(user) = lines.next() { - if let Some(pass) = lines.next() { - if !user.is_empty () && !pass.is_empty() { - return Ok((user.to_owned(), pass.to_owned())); - } - } - } - - Err("The credential file is invalid!") -} - -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("credentialfile") - .short("c") - .value_name("CREDENTIAL_FILE") - .help("specify the path to a file which contains the username and password for the inwx account seperated by a newline") - .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) - ) - .arg(Arg::with_name("nodnscheck") - .long("no-dns-check") - .help("don't wait for the dns record to be publicly visible") - ) - .arg(Arg::with_name("waitinterval") - .long("wait-interval") - .value_name("SECONDS") - .help("the amount of seconds to wait after creating a record") - .takes_value(true) - .default_value("5") - ) - ) - .subcommand(SubCommand::with_name("delete") - .about("delete a TXT record") - .arg(Arg::with_name("credentialfile") - .short("c") - .value_name("CREDENTIAL_FILE") - .help("specify the path to a file which contains the username and password for the inwx account seperated by a newline") - .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 domain = lookup_real_domain(matches.value_of("domain").unwrap()); - let value = matches.value_of("value").unwrap(); - let (user, pass) = read_credentials(matches.value_of("credentialfile").unwrap())?; - let wait_interval = matches.value_of("waitinterval").unwrap().parse().map_err(|_| "Invalid wait interval!")?; - - println!("Creating TXT record..."); - - execute_api_commands(&user, &pass, |api| { - api.create_txt_record(&domain, &value)?; - Ok(()) - })?; - - println!("=> done!"); - - if !matches.is_present("nodnscheck") { - 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(&domain, value) { - break; - } - - wait_secs *= 2; - - sleep(Duration::from_secs(wait_secs)); - } - - println!("=> done!"); - } - - if wait_interval > 0 { - println!("Waiting {} additional seconds...", &wait_interval); - - sleep(Duration::from_secs(wait_interval)); - - println!("=> done!"); - } - } else if let Some(matches) = matches.subcommand_matches("delete") { - let domain = lookup_real_domain(matches.value_of("domain").unwrap()); - let (user, pass) = read_credentials(matches.value_of("credentialfile").unwrap())?; - - println!("Deleting TXT record..."); - - execute_api_commands(&user, &pass, |api| { - api.delete_txt_record(&domain)?; - Ok(()) - })?; - - println!("=> done!"); - } else { - app.print_help().unwrap(); - std::process::exit(1); - } - - Ok(()) -} - -fn execute_api_commands(user: &str, pass: &str, op: F) -> Result<(), String> where F: Fn(&mut Inwx) -> Result<(), InwxError> { - let mut api = Inwx::new(&user, &pass).map_err(|err| format!("{}", err))?; - - let mut err = None; - match op(&mut api) { - Err(e) => { - err = Some(e); - }, - _ => {} - } - - if let Err(e) = api.logout() { - if let None = err { - err = Some(e); - } - } - - match err { - Some(e) => Err(format!("{}", e)), - None => Ok(()) - } -} fn main() { openssl_probe::init_ssl_cert_env_vars(); - if let Err(msg) = run() { + if let Err(msg) = cli::run() { eprintln!("=> Error: {}", msg); exit(1); } diff --git a/src/rpc.rs b/src/rpc.rs index 311ccbd..ce7203a 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -10,14 +10,20 @@ use cookie::{Cookie, CookieJar}; #[derive(Debug)] pub enum RpcError { ConnectionError(reqwest::Error), - InvalidResponse + InvalidResponse, + ApiError { + method: String, + reason: String, + msg: String + } } impl fmt::Display for RpcError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { &RpcError::InvalidResponse => write!(f, "The inwx api did not return a valid response"), - &RpcError::ConnectionError(ref e) => write!(f, "Could not connect to the inwx api: {}", e) + &RpcError::ConnectionError(ref e) => write!(f, "Could not connect to the inwx api: {}", e), + &RpcError::ApiError { ref method, ref msg, ref reason } => write!(f, "The inwx api did return an error: method={}, msg={}, reason={}", method, msg, reason) } } } @@ -92,10 +98,6 @@ impl RpcRequest { } } - pub fn get_method(&self) -> String { - self.method.clone() - } - pub fn send(self, url: &str, cookies: &mut CookieJar) -> Result { let client = Client::new(); @@ -115,17 +117,16 @@ impl RpcRequest { let response = request.send().map_err(|e| RpcError::ConnectionError(e))?; - RpcResponse::new(response, cookies).ok_or(RpcError::InvalidResponse) + RpcResponse::new(response, self.method, cookies) } } pub struct RpcResponse { - success: bool, - package: Package, + package: Package } impl RpcResponse { - fn new(mut response: Response, cookies: &mut CookieJar) -> Option { + fn new(mut response: Response, method: String, cookies: &mut CookieJar) -> Result { if response.status() == StatusCode::OK { if let Ok(ref response_text) = response.text() { if let Ok(package) = parser::parse(response_text) { @@ -147,19 +148,39 @@ impl RpcResponse { } } - return Some(RpcResponse { - success, - package, + let mut msg = String::new(); + let mut reason = String::new(); + + if !success { + if let Ok(value) = evaluate_xpath( + &package.as_document(), + "/methodResponse/params/param/value/struct/member[name/text()=\"msg\"]/value/string/text()" + ) { + msg = value.string(); + } + + if let Ok(value) = evaluate_xpath( + &package.as_document(), + "/methodResponse/params/param/value/struct/member[name/text()=\"reason\"]/value/string/text()" + ) { + reason = value.string(); + } + + return Err(RpcError::ApiError { + method, + msg, + reason + }); + } + + return Ok(RpcResponse { + package }); } } } - None - } - - pub fn is_success(&self) -> bool { - self.success + Err(RpcError::InvalidResponse) } pub fn get_document(&self) -> Document {