diff --git a/.dockerignore b/.dockerignore index d0f2d9a..e390dec 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ -/.git/ +/.vscode +/.git /.gitignore /.travis-yml /target /LICENSE.txt -/README.md \ No newline at end of file +/README.md +/config.json diff --git a/.editorconfig b/.editorconfig index a9a8b01..80ce1de 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,11 +3,10 @@ root = true [*] charset = utf-8 end_of_line = lf -insert_final_newline = false -indent_style = tab +insert_final_newline = true +indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.yml] -indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/.travis.yml b/.travis.yml index 858b420..e2a35fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,4 @@ deploy: - deploy/* skip_cleanup: true on: - tags: true \ No newline at end of file + tags: true diff --git a/Cargo.toml b/Cargo.toml index 9f7894a..c96bc1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ repository = "https://github.com/kegato/letsencrypt-inwx" depends = "" extended-description = "A small cli utility for automating the letsencrypt dns-01 challenge for domains hosted by inwx. The dns-01 challenge is required for obtaining wildcard certificates from letsencrypt." assets = [ - ["target/release/letsencrypt-inwx", "usr/bin/", "755"], - ["etc/certbot-inwx-auth", "usr/lib/letsencrypt-inwx/", "755"], - ["etc/certbot-inwx-cleanup", "usr/lib/letsencrypt-inwx/", "755"] + ["target/release/letsencrypt-inwx", "usr/bin/", "755"], + ["etc/certbot-inwx-auth", "usr/lib/letsencrypt-inwx/", "755"], + ["etc/certbot-inwx-cleanup", "usr/lib/letsencrypt-inwx/", "755"] ] [dependencies] diff --git a/LICENSE.txt b/LICENSE.txt index 48f8c8a..d0c7fe1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index b341acb..4f60127 100644 --- a/README.md +++ b/README.md @@ -18,27 +18,27 @@ You can store the configuration file at `/etc/letsencrypt-inwx.json` or at `~/.c The configuration file should look like this (without the comments): ```json { - "accounts": [ - { - "username": "user", - "password": "pass", - // optional, if the domain is not configured all accounts will be tried - "domains": [ - "example.com" - ], - // optional, if true the public inwx test server will be used - "ote": false - } - ], - // optional - "options": { - // optional, if true letsencrypt-inwx will not wait until the created record is publicly visible, default: false - "no_dns_check": false, - // optional, the amount of time in seconds to wait after creating a record, default: 5 seconds - "wait_interval": 5, - // optional: the dns server to use, default: the google public dns server - "dns_server": "8.8.8.8" - } + "accounts": [ + { + "username": "user", + "password": "pass", + // optional, if the domain is not configured all accounts will be tried + "domains": [ + "example.com" + ], + // optional, if true the public inwx test server will be used + "ote": false + } + ], + // optional + "options": { + // optional, if true letsencrypt-inwx will not wait until the created record is publicly visible, default: false + "no_dns_check": false, + // optional, the amount of time in seconds to wait after creating a record, default: 5 seconds + "wait_interval": 5, + // optional: the dns server to use, default: the google public dns server + "dns_server": "8.8.8.8" + } } ``` diff --git a/etc/certbot-inwx-auth b/etc/certbot-inwx-auth index cb401dc..f7ddebd 100755 --- a/etc/certbot-inwx-auth +++ b/etc/certbot-inwx-auth @@ -3,7 +3,7 @@ CONFIG_PATH=/etc/letsencrypt-inwx.json if [ -f ~/.config/letsencrypt-inwx.json ]; then - CONFIG_PATH=~/.config/letsencrypt-inwx.json + CONFIG_PATH=~/.config/letsencrypt-inwx.json fi /usr/bin/letsencrypt-inwx create -c $CONFIG_PATH -d "_acme-challenge.$CERTBOT_DOMAIN" -v "$CERTBOT_VALIDATION" diff --git a/etc/certbot-inwx-cleanup b/etc/certbot-inwx-cleanup index 7ef85a3..85adce2 100755 --- a/etc/certbot-inwx-cleanup +++ b/etc/certbot-inwx-cleanup @@ -3,7 +3,7 @@ CONFIG_PATH=/etc/letsencrypt-inwx.json if [ -f ~/.config/letsencrypt-inwx.json ]; then - CONFIG_PATH=~/.config/letsencrypt-inwx.json + CONFIG_PATH=~/.config/letsencrypt-inwx.json fi /usr/bin/letsencrypt-inwx delete -c $CONFIG_PATH -d "_acme-challenge.$CERTBOT_DOMAIN" diff --git a/etc/docker-entrypoint.sh b/etc/docker-entrypoint.sh index 053175c..3544845 100755 --- a/etc/docker-entrypoint.sh +++ b/etc/docker-entrypoint.sh @@ -3,21 +3,21 @@ CONF_CREATED=false if [ ! -z "$INWX_USER" -a ! -z "$INWX_PASSWD" ]; then - CONF_CREATED=true - >&2 echo "\ + CONF_CREATED=true + >&2 echo "\ !!! WARNING !!! PASSING INWX_USER AND INWX_PASSWD AS ENV VARIABLES IS DEPRECATED AND WILL BE REMOVED IN THE FUTURE! You should mount a config file into the container instead. See https://github.com/kegato/letsencrypt-inwx for details. " - cat << EOF > /etc/letsencrypt-inwx.json + cat << EOF > /etc/letsencrypt-inwx.json { - "accounts": [{ - "username": "$INWX_USER", - "password": "$INWX_PASSWD" - }] + "accounts": [{ + "username": "$INWX_USER", + "password": "$INWX_PASSWD" + }] } EOF - chmod 600 /etc/letsencrypt-inwx.json + chmod 600 /etc/letsencrypt-inwx.json fi set -x @@ -25,5 +25,5 @@ certbot -n --agree-tos $@ set +x if [ $CONF_CREATED = true ]; then - rm /etc/letsencrypt-inwx.json + rm /etc/letsencrypt-inwx.json fi diff --git a/src/cli.rs b/src/cli.rs index ea08882..29928a5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,182 +8,182 @@ 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 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)) + 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))?) + 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..."); + println!("Creating TXT record..."); - let is_ote = execute_api_commands(&config, &domain, |api| { - api.create_txt_record(&domain, &value)?; - Ok(()) - })?; + let is_ote = execute_api_commands(&config, &domain, |api| { + api.create_txt_record(&domain, &value)?; + Ok(()) + })?; - println!("=> done!"); + println!("=> done!"); - if !is_ote && !config.options.no_dns_check { - println!("Waiting for the dns record to be publicly visible..."); + 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; + 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()); - } + 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; - } + if check_txt_record(&config.options.dns_server, &domain, value) { + break; + } - wait_secs *= 2; + wait_secs *= 2; - sleep(Duration::from_secs(wait_secs)); - } + sleep(Duration::from_secs(wait_secs)); + } - println!("=> done!"); - } + println!("=> done!"); + } - if config.options.wait_interval > 0 { - println!("Waiting {} additional seconds...", &config.options.wait_interval); + if config.options.wait_interval > 0 { + println!("Waiting {} additional seconds...", &config.options.wait_interval); - sleep(Duration::from_secs(config.options.wait_interval)); + sleep(Duration::from_secs(config.options.wait_interval)); - println!("=> done!"); - } + println!("=> done!"); + } - Ok(()) + Ok(()) } fn delete(config: &Config, domain: &str) -> Result<(), String> { - println!("Deleting TXT record..."); + println!("Deleting TXT record..."); - execute_api_commands(&config, &domain, |api| { - api.delete_txt_record(&domain)?; - Ok(()) - })?; + execute_api_commands(&config, &domain, |api| { + api.delete_txt_record(&domain)?; + Ok(()) + })?; - println!("=> done!"); + println!("=> done!"); - Ok(()) + 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(()) + 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 index 6611085..2a3e0c0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,43 +3,43 @@ use serde::Deserialize; #[derive(Deserialize, Debug, Clone)] #[serde(default)] pub struct Config { - pub accounts: Vec, - pub options: Options + pub accounts: Vec, + pub options: Options } impl Default for Config { - fn default() -> Config { - Config { - accounts: vec!(), - options: Options::default() - } - } + 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 + 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 + 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() - } - } + 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 0fea8c4..e98b442 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -5,85 +5,85 @@ use trust_dns::op::DnsResponse; use trust_dns::rr::{DNSClass, Name, RData, Record, RecordType}; fn dns_client(dns_server: &str) -> SyncClient { - let address = format!("{}:53", dns_server).parse().unwrap(); - let conn = UdpClientConnection::new(address).unwrap(); - SyncClient::new(conn) + let address = format!("{}:53", dns_server).parse().unwrap(); + let conn = UdpClientConnection::new(address).unwrap(); + SyncClient::new(conn) } pub fn add_trailing_dot(domain: &str) -> String { - let mut domain = domain.to_owned(); + let mut domain = domain.to_owned(); - if !domain.ends_with(".") { - domain += "."; - } + if !domain.ends_with(".") { + domain += "."; + } - domain + domain } pub fn remove_trailing_dot(domain: &str) -> String { - let mut domain = domain.to_owned(); + let mut domain = domain.to_owned(); - if domain.ends_with(".") { - domain.pop(); - } + if domain.ends_with(".") { + domain.pop(); + } - domain + domain } 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(); - - for record in answers { - if let RData::CNAME(ref cname) = record.rdata() { - return Some(remove_trailing_dot(&cname.to_utf8())); - } - } - - None + 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(); + + for record in answers { + if let RData::CNAME(ref cname) = record.rdata() { + return Some(remove_trailing_dot(&cname.to_utf8())); + } + } + + None } pub fn lookup_real_domain(dns_server: &str, domain: &str) -> String { - let mut depth = 0; + let mut depth = 0; - let mut domain = domain.to_owned(); - while let Some(real_name) = check_cname(dns_server, &domain) { - domain = real_name; + let mut domain = domain.to_owned(); + while let Some(real_name) = check_cname(dns_server, &domain) { + domain = real_name; - if depth >= 10 { - break; - } + if depth >= 10 { + break; + } - depth += 1; - } + depth += 1; + } - domain + domain } 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 - }; - - if let Ok(response) = client.query(&name, DNSClass::IN, RecordType::TXT) { - for record in response.answers() { - if record.name().to_utf8().to_lowercase() == name.to_utf8().to_lowercase() { - if let RData::TXT(data) = record.rdata() { - for data in data.txt_data().iter() { - let data = String::from_utf8_lossy(data); - - if data == value { - return true; - } - } - } - } - } - } - - false + let client = dns_client(dns_server); + let name = match Name::from_str(&add_trailing_dot(domain)) { + Ok(name) => name, + Err(_) => return false + }; + + if let Ok(response) = client.query(&name, DNSClass::IN, RecordType::TXT) { + for record in response.answers() { + if record.name().to_utf8().to_lowercase() == name.to_utf8().to_lowercase() { + if let RData::TXT(data) = record.rdata() { + for data in data.txt_data().iter() { + let data = String::from_utf8_lossy(data); + + if data == value { + return true; + } + } + } + } + } + } + + false } diff --git a/src/inwx.rs b/src/inwx.rs index f696f1e..747a645 100644 --- a/src/inwx.rs +++ b/src/inwx.rs @@ -9,196 +9,196 @@ const OTE_API_URL: &str = "https://api.ote.domrobot.com/xmlrpc/"; #[derive(Debug)] pub enum InwxError { - RpcError(RpcError), - DomainNotFound, - RecordNotFound + RpcError(RpcError), + DomainNotFound, + RecordNotFound } impl fmt::Display for InwxError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - &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") - } - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &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") + } + } } impl From for InwxError { - fn from(rpc_error: RpcError) -> InwxError { - InwxError::RpcError(rpc_error) - } + fn from(rpc_error: RpcError) -> InwxError { + InwxError::RpcError(rpc_error) + } } pub struct Inwx<'a> { - cookies: CookieJar, - account: &'a Account + cookies: CookieJar, + account: &'a Account } 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)?; - - Ok(response) - } - - fn login(&mut self) -> Result<(), InwxError> { - let request = RpcRequest::new("account.login", &[ - RpcRequestParameter { - name: "user", - value: RpcRequestParameterValue::String(self.account.username.to_owned()) - }, - RpcRequestParameter { - name: "pass", - value: RpcRequestParameterValue::String(self.account.password.to_owned()) - } - ]); - - self.send_request(request)?; - - Ok(()) - } - - pub fn new(account: &'a Account) -> Result, InwxError> { - let mut api = Inwx { - cookies: CookieJar::new(), - account - }; - - api.login()?; - - Ok(api) - } - - fn split_domain(&mut self, domain: &str) -> Result<(String, String), InwxError> { - let page_size = 20; - let mut page = 1; - - loop { - let request = RpcRequest::new("nameserver.list", &[ - RpcRequestParameter { - name: "pagelimit", - value: RpcRequestParameterValue::Int(page_size) - }, - RpcRequestParameter { - name: "page", - value: RpcRequestParameterValue::Int(page) - } - ]); - - let response = self.send_request(request)?; - - let total: i32 = evaluate_xpath( - &response.get_document(), - "/methodResponse/params/param/value/struct/member[name/text()=\"resData\"]/value/struct/member[name/text()=\"count\"]/value/int" - ) - .ok() - .and_then(|value| value.string().parse().ok()) - .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 { - if let Some(ref text) = node.text() { - let domain_root = text.text(); - - if domain.ends_with(&format!(".{}", domain_root)) { - let mut name = &domain[0..domain.len() - domain_root.len() - 1]; - - return Ok((domain_root.to_owned(), name.to_owned())); - } else if domain == domain_root { - return Ok((domain_root.to_owned(), "".to_owned())); - } - } - } - } - - if total > page * page_size { - page += 1; - } else { - return Err(InwxError::DomainNotFound); - } - } - } - - pub fn create_txt_record(&mut self, domain: &str, content: &str) -> Result<(), InwxError> { - let (domain, name) = self.split_domain(domain)?; - - let request = RpcRequest::new("nameserver.createRecord", &[ - RpcRequestParameter { - name: "type", - value: RpcRequestParameterValue::String("TXT".to_owned()) - }, - RpcRequestParameter { - name: "name", - value: RpcRequestParameterValue::String(name) - }, - RpcRequestParameter { - name: "content", - value: RpcRequestParameterValue::String(content.to_owned()) - }, - RpcRequestParameter { - name: "domain", - value: RpcRequestParameterValue::String(domain) - } - ]); - - self.send_request(request)?; - - Ok(()) - } - - pub fn get_record_id(&mut self, domain: &str) -> Result { - let (domain, name) = self.split_domain(domain)?; - - let request = RpcRequest::new("nameserver.info", &[ - RpcRequestParameter { - name: "type", - value: RpcRequestParameterValue::String("TXT".to_owned()) - }, - RpcRequestParameter { - name: "name", - value: RpcRequestParameterValue::String(name.to_owned()) - }, - RpcRequestParameter { - name: "domain", - value: RpcRequestParameterValue::String(domain.to_owned()) - } - ]); - - let response = self.send_request(request)?; - - let id = match evaluate_xpath(&response.get_document(), "/methodResponse/params/param/value/struct/member[name/text()=\"resData\"]/value/struct/member[name/text()=\"record\"]/value/array/data/value[1]/struct/member[name/text()=\"id\"]/value/int/text()") { - Ok(ref id) => id.string().parse::().ok(), - Err(_) => None - }; - - id.ok_or_else(|| InwxError::RecordNotFound) - } - - pub fn delete_txt_record(&mut self, domain: &str) -> Result<(), InwxError> { - let id = self.get_record_id(domain)?; - - let request = RpcRequest::new("nameserver.deleteRecord", &[ - RpcRequestParameter { - name: "id", - value: RpcRequestParameterValue::Int(id) - } - ]); - - self.send_request(request)?; - - Ok(()) - } - - pub fn logout(mut self) -> Result<(), InwxError> { - let request = RpcRequest::new("account.logout", &[]); - - self.send_request(request)?; - - Ok(()) - } + 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)?; + + Ok(response) + } + + fn login(&mut self) -> Result<(), InwxError> { + let request = RpcRequest::new("account.login", &[ + RpcRequestParameter { + name: "user", + value: RpcRequestParameterValue::String(self.account.username.to_owned()) + }, + RpcRequestParameter { + name: "pass", + value: RpcRequestParameterValue::String(self.account.password.to_owned()) + } + ]); + + self.send_request(request)?; + + Ok(()) + } + + pub fn new(account: &'a Account) -> Result, InwxError> { + let mut api = Inwx { + cookies: CookieJar::new(), + account + }; + + api.login()?; + + Ok(api) + } + + fn split_domain(&mut self, domain: &str) -> Result<(String, String), InwxError> { + let page_size = 20; + let mut page = 1; + + loop { + let request = RpcRequest::new("nameserver.list", &[ + RpcRequestParameter { + name: "pagelimit", + value: RpcRequestParameterValue::Int(page_size) + }, + RpcRequestParameter { + name: "page", + value: RpcRequestParameterValue::Int(page) + } + ]); + + let response = self.send_request(request)?; + + let total: i32 = evaluate_xpath( + &response.get_document(), + "/methodResponse/params/param/value/struct/member[name/text()=\"resData\"]/value/struct/member[name/text()=\"count\"]/value/int" + ) + .ok() + .and_then(|value| value.string().parse().ok()) + .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 { + if let Some(ref text) = node.text() { + let domain_root = text.text(); + + if domain.ends_with(&format!(".{}", domain_root)) { + let mut name = &domain[0..domain.len() - domain_root.len() - 1]; + + return Ok((domain_root.to_owned(), name.to_owned())); + } else if domain == domain_root { + return Ok((domain_root.to_owned(), "".to_owned())); + } + } + } + } + + if total > page * page_size { + page += 1; + } else { + return Err(InwxError::DomainNotFound); + } + } + } + + pub fn create_txt_record(&mut self, domain: &str, content: &str) -> Result<(), InwxError> { + let (domain, name) = self.split_domain(domain)?; + + let request = RpcRequest::new("nameserver.createRecord", &[ + RpcRequestParameter { + name: "type", + value: RpcRequestParameterValue::String("TXT".to_owned()) + }, + RpcRequestParameter { + name: "name", + value: RpcRequestParameterValue::String(name) + }, + RpcRequestParameter { + name: "content", + value: RpcRequestParameterValue::String(content.to_owned()) + }, + RpcRequestParameter { + name: "domain", + value: RpcRequestParameterValue::String(domain) + } + ]); + + self.send_request(request)?; + + Ok(()) + } + + pub fn get_record_id(&mut self, domain: &str) -> Result { + let (domain, name) = self.split_domain(domain)?; + + let request = RpcRequest::new("nameserver.info", &[ + RpcRequestParameter { + name: "type", + value: RpcRequestParameterValue::String("TXT".to_owned()) + }, + RpcRequestParameter { + name: "name", + value: RpcRequestParameterValue::String(name.to_owned()) + }, + RpcRequestParameter { + name: "domain", + value: RpcRequestParameterValue::String(domain.to_owned()) + } + ]); + + let response = self.send_request(request)?; + + let id = match evaluate_xpath(&response.get_document(), "/methodResponse/params/param/value/struct/member[name/text()=\"resData\"]/value/struct/member[name/text()=\"record\"]/value/array/data/value[1]/struct/member[name/text()=\"id\"]/value/int/text()") { + Ok(ref id) => id.string().parse::().ok(), + Err(_) => None + }; + + id.ok_or_else(|| InwxError::RecordNotFound) + } + + pub fn delete_txt_record(&mut self, domain: &str) -> Result<(), InwxError> { + let id = self.get_record_id(domain)?; + + let request = RpcRequest::new("nameserver.deleteRecord", &[ + RpcRequestParameter { + name: "id", + value: RpcRequestParameterValue::Int(id) + } + ]); + + self.send_request(request)?; + + Ok(()) + } + + pub fn logout(mut self) -> Result<(), InwxError> { + let request = RpcRequest::new("account.logout", &[]); + + self.send_request(request)?; + + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index d2c51dd..667247c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,10 +17,10 @@ mod cli; use std::process::exit; fn main() { - openssl_probe::init_ssl_cert_env_vars(); + openssl_probe::init_ssl_cert_env_vars(); - if let Err(msg) = cli::run() { - eprintln!("=> Error: {}", msg); - exit(1); - } + if let Err(msg) = cli::run() { + eprintln!("=> Error: {}", msg); + exit(1); + } } diff --git a/src/rpc.rs b/src/rpc.rs index ce7203a..9c36dba 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -9,181 +9,181 @@ use cookie::{Cookie, CookieJar}; #[derive(Debug)] pub enum RpcError { - ConnectionError(reqwest::Error), - InvalidResponse, - ApiError { - method: String, - reason: String, - msg: String - } + ConnectionError(reqwest::Error), + 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::ApiError { ref method, ref msg, ref reason } => write!(f, "The inwx api did return an error: method={}, msg={}, reason={}", method, msg, reason) - } - } + 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::ApiError { ref method, ref msg, ref reason } => write!(f, "The inwx api did return an error: method={}, msg={}, reason={}", method, msg, reason) + } + } } pub struct RpcRequestParameter { - pub name: &'static str, - pub value: RpcRequestParameterValue, + pub name: &'static str, + pub value: RpcRequestParameterValue, } pub enum RpcRequestParameterValue { - String(String), - Int(i32), + String(String), + Int(i32), } pub struct RpcRequest { - body: Vec, - method: String + body: Vec, + method: String } impl RpcRequest { - pub fn new(method: &str, parameters: &[RpcRequestParameter]) -> RpcRequest { - let package = Package::new(); - let doc = package.as_document(); - - let method_call = doc.create_element("methodCall"); - doc.root().append_child(method_call); - - let method_name = doc.create_element("methodName"); - method_name.append_child(doc.create_text(method)); - method_call.append_child(method_name); - - let params = doc.create_element("params"); - method_call.append_child(params); - let param = doc.create_element("param"); - params.append_child(param); - let value = doc.create_element("value"); - param.append_child(value); - let s = doc.create_element("struct"); - value.append_child(s); - - for param in parameters { - let member = doc.create_element("member"); - - let name = doc.create_element("name"); - name.append_child(doc.create_text(¶m.name)); - member.append_child(name); - - let value = doc.create_element("value"); - member.append_child(value); - match param.value { - RpcRequestParameterValue::String(ref val) => { - let string = doc.create_element("string"); - string.append_child(doc.create_text(val)); - value.append_child(string); - } - RpcRequestParameterValue::Int(ref val) => { - let string = doc.create_element("int"); - string.append_child(doc.create_text(&val.to_string())); - value.append_child(string); - } - } - - s.append_child(member); - } - - let mut body = Vec::new(); - format_document(&doc, &mut body).unwrap(); - - RpcRequest { - body: body, - method: method.to_owned(), - } - } - - pub fn send(self, url: &str, cookies: &mut CookieJar) -> Result { - let client = Client::new(); - - let mut request = client - .post(url) - .body(self.body); - - let cookie_values: Vec = cookies - .iter() - .map(|cookie| format!("{}", cookie.encoded())) - .collect(); - - if cookie_values.len() > 0 { - let cookie_values = cookie_values.join(";"); - request = request.header(reqwest::header::COOKIE, cookie_values); - } - - let response = request.send().map_err(|e| RpcError::ConnectionError(e))?; - - RpcResponse::new(response, self.method, cookies) - } + pub fn new(method: &str, parameters: &[RpcRequestParameter]) -> RpcRequest { + let package = Package::new(); + let doc = package.as_document(); + + let method_call = doc.create_element("methodCall"); + doc.root().append_child(method_call); + + let method_name = doc.create_element("methodName"); + method_name.append_child(doc.create_text(method)); + method_call.append_child(method_name); + + let params = doc.create_element("params"); + method_call.append_child(params); + let param = doc.create_element("param"); + params.append_child(param); + let value = doc.create_element("value"); + param.append_child(value); + let s = doc.create_element("struct"); + value.append_child(s); + + for param in parameters { + let member = doc.create_element("member"); + + let name = doc.create_element("name"); + name.append_child(doc.create_text(¶m.name)); + member.append_child(name); + + let value = doc.create_element("value"); + member.append_child(value); + match param.value { + RpcRequestParameterValue::String(ref val) => { + let string = doc.create_element("string"); + string.append_child(doc.create_text(val)); + value.append_child(string); + } + RpcRequestParameterValue::Int(ref val) => { + let string = doc.create_element("int"); + string.append_child(doc.create_text(&val.to_string())); + value.append_child(string); + } + } + + s.append_child(member); + } + + let mut body = Vec::new(); + format_document(&doc, &mut body).unwrap(); + + RpcRequest { + body: body, + method: method.to_owned(), + } + } + + pub fn send(self, url: &str, cookies: &mut CookieJar) -> Result { + let client = Client::new(); + + let mut request = client + .post(url) + .body(self.body); + + let cookie_values: Vec = cookies + .iter() + .map(|cookie| format!("{}", cookie.encoded())) + .collect(); + + if cookie_values.len() > 0 { + let cookie_values = cookie_values.join(";"); + request = request.header(reqwest::header::COOKIE, cookie_values); + } + + let response = request.send().map_err(|e| RpcError::ConnectionError(e))?; + + RpcResponse::new(response, self.method, cookies) + } } pub struct RpcResponse { - package: Package + package: Package } impl RpcResponse { - 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) { - let mut success = false; - - for header in response.headers().get_all(reqwest::header::SET_COOKIE) { - if let Ok(value) = header.to_str() { - if let Ok(cookie) = Cookie::parse(value.to_owned()) { - cookies.add(cookie); - } - } - } - - if let Ok(code) = evaluate_xpath(&package.as_document(), "/methodResponse/params/param/value/struct/member[name/text()=\"code\"]/value/int/text()") { - if let Ok(code) = code.string().parse::() { - if code < 2000 { - success = true; - } - } - } - - 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 - }); - } - } - } - - Err(RpcError::InvalidResponse) - } - - pub fn get_document(&self) -> Document { - self.package.as_document() - } + 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) { + let mut success = false; + + for header in response.headers().get_all(reqwest::header::SET_COOKIE) { + if let Ok(value) = header.to_str() { + if let Ok(cookie) = Cookie::parse(value.to_owned()) { + cookies.add(cookie); + } + } + } + + if let Ok(code) = evaluate_xpath(&package.as_document(), "/methodResponse/params/param/value/struct/member[name/text()=\"code\"]/value/int/text()") { + if let Ok(code) = code.string().parse::() { + if code < 2000 { + success = true; + } + } + } + + 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 + }); + } + } + } + + Err(RpcError::InvalidResponse) + } + + pub fn get_document(&self) -> Document { + self.package.as_document() + } }