diff --git a/.dockerignore b/.dockerignore index 2afe84b..d0f2d9a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,6 @@ -/.git/ -/.gitignore -/.travis-yml -/target -/LICENSE.txt +/.git/ +/.gitignore +/.travis-yml +/target +/LICENSE.txt /README.md \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a9a8b01 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = false +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = true + +[*.yml] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 53cc324..4864d06 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/Dockerfile b/Dockerfile index b28f40e..cb323ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM ekidd/rust-musl-builder:stable as builder -COPY . . -RUN cargo install cargo-deb -RUN cargo deb --target x86_64-unknown-linux-musl - -FROM certbot/certbot:rolling -VOLUME /etc/letsencrypt -COPY --from=builder /home/rust/src/target/x86_64-unknown-linux-musl/release/letsencrypt-inwx /usr/bin/ -COPY etc/* /usr/lib/letsencrypt-inwx/ - +FROM ekidd/rust-musl-builder:stable as builder +COPY . . +RUN cargo install cargo-deb +RUN cargo deb --target x86_64-unknown-linux-musl + +FROM certbot/certbot:rolling +VOLUME /etc/letsencrypt +COPY --from=builder /home/rust/src/target/x86_64-unknown-linux-musl/release/letsencrypt-inwx /usr/bin/ +COPY etc/* /usr/lib/letsencrypt-inwx/ + ENTRYPOINT ["/bin/sh", "/usr/lib/letsencrypt-inwx/docker-entrypoint.sh"] \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index 9836591..48f8c8a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Matthias Herzog - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -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 +MIT License + +Copyright (c) 2018 Matthias Herzog + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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 diff --git a/README.md b/README.md index 807cecc..72c1d67 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,50 @@ -# letsencrypt-inwx [![Build Status](https://travis-ci.org/kegato/letsencrypt-inwx.svg?branch=master)](https://travis-ci.org/kegato/letsencrypt-inwx) [![Docker Build Status](https://img.shields.io/docker/build/kegato/letsencrypt-inwx.svg)](https://hub.docker.com/r/kegato/letsencrypt-inwx/) [![Crates.io](https://img.shields.io/crates/v/letsencrypt-inwx.svg)](https://crates.io/crates/letsencrypt-inwx) - -A small cli utility for automating the letsencrypt dns-01 challenge for domains hosted by inwx. This allows you to obtain wildcard certificates from letsencrypt. - -## Installation -### Ubuntu / Debian -- Build the .deb package or download it from [releases](https://github.com/kegato/letsencrypt-inwx/releases/latest) and install it with `sudo dpkg -i ` - -### Other linux -- Build the executable or download it from [releases](https://github.com/kegato/letsencrypt-inwx/releases/latest) and copy it to `/usr/bin/` -- Copy both certbot scripts from `./etc/` to `/usr/lib/letsencrypt-inwx/` - -### With cargo -- Run `cargo install letsencrypt-inwx` - -## Usage -### With certbot -- Put your inwx login data seperated by a newline into `/etc/letsencrypt-inwx-cred` -- Make sure the file is only readable for root `sudo chmod 600 /etc/letsencrypt-inwx-cred` -- You can now get certificates from [certbot](https://certbot.eff.org/) by running `sudo certbot certonly -n --agree-tos --email --server https://acme-v02.api.letsencrypt.org/directory --preferred-challenges=dns-01 --manual --manual-auth-hook /usr/lib/letsencrypt-inwx/certbot-inwx-auth --manual-cleanup-hook /usr/lib/letsencrypt-inwx/certbot-inwx-cleanup --manual-public-ip-logging-ok -d ` - -#### Notes -- You need atleast certbot 0.22.0 to issue wildcard certificates. -- You can put your inwx login data into `~/.config/letsencrypt-inwx-cred` if you want to run certbot as non-root user - -### With Docker and certbot -- Put your inwx login data into a docker env file like this -```sh -INWX_USER=username -INWX_PASSWD=password -``` -- Generate your certificate by running `docker run --rm -it --env-file -v /etc/letsencrypt:/etc/letsencrypt kegato/letsencrypt-inwx certonly --email --preferred-challenges=dns-01 --manual --manual-auth-hook /usr/lib/letsencrypt-inwx/certbot-inwx-auth --manual-cleanup-hook /usr/lib/letsencrypt-inwx/certbot-inwx-cleanup --manual-public-ip-logging-ok -d ` -- Your certificate is now at `/etc/letsencrypt/live//` -- You can renew your certificate by running `docker run --rm -it --env-file -v /etc/letsencrypt:/etc/letsencrypt kegato/letsencrypt-inwx renew` - -### Manually -- Put your inwx login data seperated by a newline into a file -- Create a txt record with `letsencrypt-inwx create -c -d _acme-challenge.your-domain.com -v ` -- Delete it with `letsencrypt-inwx delete -c -d _acme-challenge.your-domain.com` - -## Building -### Requirements -`libssl-dev` and `pkg-config` are required when building on Ubuntu / Debian see [here](https://github.com/sfackler/rust-openssl). - -### .deb package -- Install [cargo-deb](https://github.com/mmstick/cargo-deb) by running `cargo install cargo-deb` -- Run `cargo deb` to build the package - -### only the executable +# letsencrypt-inwx [![Build Status](https://travis-ci.org/kegato/letsencrypt-inwx.svg?branch=master)](https://travis-ci.org/kegato/letsencrypt-inwx) [![Docker Build Status](https://img.shields.io/docker/build/kegato/letsencrypt-inwx.svg)](https://hub.docker.com/r/kegato/letsencrypt-inwx/) [![Crates.io](https://img.shields.io/crates/v/letsencrypt-inwx.svg)](https://crates.io/crates/letsencrypt-inwx) + +A small cli utility for automating the letsencrypt dns-01 challenge for domains hosted by inwx. This allows you to obtain wildcard certificates from letsencrypt. + +## Installation +### Ubuntu / Debian +- Build the .deb package or download it from [releases](https://github.com/kegato/letsencrypt-inwx/releases/latest) and install it with `sudo dpkg -i ` + +### Other linux +- Build the executable or download it from [releases](https://github.com/kegato/letsencrypt-inwx/releases/latest) and copy it to `/usr/bin/` +- Copy both certbot scripts from `./etc/` to `/usr/lib/letsencrypt-inwx/` + +### With cargo +- Run `cargo install letsencrypt-inwx` + +## Usage +### With certbot +- Put your inwx login data seperated by a newline into `/etc/letsencrypt-inwx-cred` +- Make sure the file is only readable for root `sudo chmod 600 /etc/letsencrypt-inwx-cred` +- You can now get certificates from [certbot](https://certbot.eff.org/) by running `sudo certbot certonly -n --agree-tos --email --server https://acme-v02.api.letsencrypt.org/directory --preferred-challenges=dns-01 --manual --manual-auth-hook /usr/lib/letsencrypt-inwx/certbot-inwx-auth --manual-cleanup-hook /usr/lib/letsencrypt-inwx/certbot-inwx-cleanup --manual-public-ip-logging-ok -d ` + +#### Notes +- You need atleast certbot 0.22.0 to issue wildcard certificates. +- You can put your inwx login data into `~/.config/letsencrypt-inwx-cred` if you want to run certbot as non-root user + +### With Docker and certbot +- Put your inwx login data into a docker env file like this +```sh +INWX_USER=username +INWX_PASSWD=password +``` +- Generate your certificate by running `docker run --rm -it --env-file -v /etc/letsencrypt:/etc/letsencrypt kegato/letsencrypt-inwx certonly --email --preferred-challenges=dns-01 --manual --manual-auth-hook /usr/lib/letsencrypt-inwx/certbot-inwx-auth --manual-cleanup-hook /usr/lib/letsencrypt-inwx/certbot-inwx-cleanup --manual-public-ip-logging-ok -d ` +- Your certificate is now at `/etc/letsencrypt/live//` +- You can renew your certificate by running `docker run --rm -it --env-file -v /etc/letsencrypt:/etc/letsencrypt kegato/letsencrypt-inwx renew` + +### Manually +- Put your inwx login data seperated by a newline into a file +- Create a txt record with `letsencrypt-inwx create -c -d _acme-challenge.your-domain.com -v ` +- Delete it with `letsencrypt-inwx delete -c -d _acme-challenge.your-domain.com` + +## Building +### Requirements +`libssl-dev` and `pkg-config` are required when building on Ubuntu / Debian see [here](https://github.com/sfackler/rust-openssl). + +### .deb package +- Install [cargo-deb](https://github.com/mmstick/cargo-deb) by running `cargo install cargo-deb` +- Run `cargo deb` to build the package + +### only the executable - Run `cargo build --release` to build the `letsencrypt-inwx` executable \ No newline at end of file diff --git a/src/dns.rs b/src/dns.rs index bff76da..9c61f75 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -1,29 +1,29 @@ -use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; -use trust_dns_resolver::Resolver; - -pub fn check_txt_record(domain: &str, value: &str) -> bool { - let mut opts = ResolverOpts::default(); - opts.cache_size = 0; - - let resolver = match Resolver::new(ResolverConfig::default(), opts) { - Ok(resolver) => resolver, - _ => return false - }; - - let result = match resolver.txt_lookup(domain) { - Ok(result) => result, - _ => return false - }; - - for record in result.iter() { - for data in record.txt_data().iter() { - let data = String::from_utf8_lossy(data); - - if data == value { - return true; - } - } - } - - false +use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; +use trust_dns_resolver::Resolver; + +pub fn check_txt_record(domain: &str, value: &str) -> bool { + let mut opts = ResolverOpts::default(); + opts.cache_size = 0; + + let resolver = match Resolver::new(ResolverConfig::default(), opts) { + Ok(resolver) => resolver, + _ => return false + }; + + let result = match resolver.txt_lookup(domain) { + Ok(result) => result, + _ => return false + }; + + for record in result.iter() { + for data in record.txt_data().iter() { + let data = String::from_utf8_lossy(data); + + if data == value { + return true; + } + } + } + + false } \ No newline at end of file diff --git a/src/inwx.rs b/src/inwx.rs index c2169ac..07608bc 100644 --- a/src/inwx.rs +++ b/src/inwx.rs @@ -7,191 +7,191 @@ const API_URL: &str = "https://api.domrobot.com/xmlrpc/"; #[derive(Debug)] pub enum InwxError { - RpcError(RpcError), - ApiError { - method: String, - msg: String - } + RpcError(RpcError), + ApiError { + method: String, + msg: String + } } 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 } => write!(f, "{}: {}", method, msg) - } - } + 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 } => write!(f, "{}: {}", method, msg) + } + } } pub struct Inwx { - cookie: Cookie + cookie: Cookie } impl Inwx { - fn send_request(request: RpcRequest) -> Result { - let method = request.get_method(); - - let response = request.send(API_URL).map_err(|e| InwxError::RpcError(e))?; - - 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() - }; - - Err(InwxError::ApiError { - msg, - method - }) - } - } - - fn login(user: &str, pass: &str) -> Result { - let request = RpcRequest::new("account.login", &[ - RpcRequestParameter { - name: "user", - value: RpcRequestParameterValue::String(user.to_owned()) - }, - RpcRequestParameter { - name: "pass", - value: RpcRequestParameterValue::String(pass.to_owned()) - } - ]); - - let response = Inwx::send_request(request)?; - - Ok(response.get_cookie()) - } - - pub fn new(user: &str, pass: &str) -> Result { - let cookie = Inwx::login(user, pass)?; - - Ok(Inwx { - cookie - }) - } - - fn split_domain(&self, domain: &str) -> Result<(String, String), InwxError> { - let mut request = RpcRequest::new("nameserver.list", &[ - RpcRequestParameter { - name: "pagelimit", - value: RpcRequestParameterValue::Int(1000) - } - ]); - request.set_cookie(self.cookie.clone()); - - let response = Inwx::send_request(request)?; - - 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())); - } - } - } - } - - Err(InwxError::ApiError { - method: "nameserver.list".to_owned(), - msg: "Domain not found".to_owned() - }) - } - - pub fn create_txt_record(&self, domain: &str, content: &str) -> Result<(), InwxError> { - let (domain, name) = self.split_domain(domain)?; - - let mut 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) - }, - RpcRequestParameter { - name: "ttl", - value: RpcRequestParameterValue::Int(300) - } - ]); - request.set_cookie(self.cookie.clone()); - - Inwx::send_request(request)?; - - Ok(()) - } - - pub fn get_record_id(&self, domain: &str) -> Result { - let (domain, name) = self.split_domain(domain)?; - - let mut 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()) - } - ]); - request.set_cookie(self.cookie.clone()); - - let response = Inwx::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(InwxError::ApiError { - method: "nameserver.info".to_owned(), - msg: "Record not found".to_owned() - }) - } - - pub fn delete_txt_record(&self, domain: &str) -> Result<(), InwxError> { - let id = self.get_record_id(domain)?; - - let mut request = RpcRequest::new("nameserver.deleteRecord", &[ - RpcRequestParameter { - name: "id", - value: RpcRequestParameterValue::Int(id) - } - ]); - request.set_cookie(self.cookie.clone()); - - Inwx::send_request(request)?; - - Ok(()) - } - - pub fn logout(self) -> Result<(), InwxError> { - let mut request = RpcRequest::new("account.logout", &[]); - request.set_cookie(self.cookie); - - Inwx::send_request(request)?; - - Ok(()) - } + fn send_request(request: RpcRequest) -> Result { + let method = request.get_method(); + + let response = request.send(API_URL).map_err(|e| InwxError::RpcError(e))?; + + 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() + }; + + Err(InwxError::ApiError { + msg, + method + }) + } + } + + fn login(user: &str, pass: &str) -> Result { + let request = RpcRequest::new("account.login", &[ + RpcRequestParameter { + name: "user", + value: RpcRequestParameterValue::String(user.to_owned()) + }, + RpcRequestParameter { + name: "pass", + value: RpcRequestParameterValue::String(pass.to_owned()) + } + ]); + + let response = Inwx::send_request(request)?; + + Ok(response.get_cookie()) + } + + pub fn new(user: &str, pass: &str) -> Result { + let cookie = Inwx::login(user, pass)?; + + Ok(Inwx { + cookie + }) + } + + fn split_domain(&self, domain: &str) -> Result<(String, String), InwxError> { + let mut request = RpcRequest::new("nameserver.list", &[ + RpcRequestParameter { + name: "pagelimit", + value: RpcRequestParameterValue::Int(1000) + } + ]); + request.set_cookie(self.cookie.clone()); + + let response = Inwx::send_request(request)?; + + 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())); + } + } + } + } + + Err(InwxError::ApiError { + method: "nameserver.list".to_owned(), + msg: "Domain not found".to_owned() + }) + } + + pub fn create_txt_record(&self, domain: &str, content: &str) -> Result<(), InwxError> { + let (domain, name) = self.split_domain(domain)?; + + let mut 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) + }, + RpcRequestParameter { + name: "ttl", + value: RpcRequestParameterValue::Int(300) + } + ]); + request.set_cookie(self.cookie.clone()); + + Inwx::send_request(request)?; + + Ok(()) + } + + pub fn get_record_id(&self, domain: &str) -> Result { + let (domain, name) = self.split_domain(domain)?; + + let mut 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()) + } + ]); + request.set_cookie(self.cookie.clone()); + + let response = Inwx::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(InwxError::ApiError { + method: "nameserver.info".to_owned(), + msg: "Record not found".to_owned() + }) + } + + pub fn delete_txt_record(&self, domain: &str) -> Result<(), InwxError> { + let id = self.get_record_id(domain)?; + + let mut request = RpcRequest::new("nameserver.deleteRecord", &[ + RpcRequestParameter { + name: "id", + value: RpcRequestParameterValue::Int(id) + } + ]); + request.set_cookie(self.cookie.clone()); + + Inwx::send_request(request)?; + + Ok(()) + } + + pub fn logout(self) -> Result<(), InwxError> { + let mut request = RpcRequest::new("account.logout", &[]); + request.set_cookie(self.cookie); + + Inwx::send_request(request)?; + + Ok(()) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 26662b2..c875c2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,169 +12,169 @@ use letsencrypt_inwx::inwx::{Inwx, InwxError}; use letsencrypt_inwx::dns::check_txt_record; 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)?; + let mut file = File::open(path)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; - Ok(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!") + 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.0.3") - .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") - ) - ) - .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 = matches.value_of("domain").unwrap(); - let value = matches.value_of("value").unwrap(); - let (user, pass) = read_credentials(matches.value_of("credentialfile").unwrap())?; - - 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!"); - } - } else if let Some(matches) = matches.subcommand_matches("delete") { - let 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(()) + let mut app = App::new("letsencrypt-inwx") + .version("1.0.3") + .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") + ) + ) + .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 = matches.value_of("domain").unwrap(); + let value = matches.value_of("value").unwrap(); + let (user, pass) = read_credentials(matches.value_of("credentialfile").unwrap())?; + + 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!"); + } + } else if let Some(matches) = matches.subcommand_matches("delete") { + let 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(&Inwx) -> Result<(), InwxError> { - let api = Inwx::new(&user, &pass).map_err(|err| format!("{}", err))?; - - let mut err = None; - match op(&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(()) - } + let api = Inwx::new(&user, &pass).map_err(|err| format!("{}", err))?; + + let mut err = None; + match op(&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(); + openssl_probe::init_ssl_cert_env_vars(); - if let Err(msg) = run() { - eprintln!("=> Error: {}", msg); - exit(1); - } + if let Err(msg) = run() { + eprintln!("=> Error: {}", msg); + exit(1); + } } \ No newline at end of file diff --git a/src/rpc.rs b/src/rpc.rs index 2dcea94..d78b512 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -10,166 +10,166 @@ use cookie; #[derive(Debug)] pub enum RpcError { - ConnectionError(reqwest::Error), - InvalidResponse + ConnectionError(reqwest::Error), + InvalidResponse } 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) - } - } + 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) + } + } } 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, - cookie: Option, - method: String, + body: Vec, + cookie: Option, + 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, - cookie: None, - method: method.to_owned(), - } - } - - pub fn set_cookie(&mut self, cookie: Cookie) { - self.cookie = Some(cookie); - } - - pub fn get_method(&self) -> String { - self.method.clone() - } - - pub fn send(self, url: &str) -> Result { - let client = Client::new(); - - let mut request = client.post(url); - request.body(self.body); - - if let Some(cookie) = self.cookie { - request.header(cookie); - } - - request.send() - .map_err(|e| RpcError::ConnectionError(e)) - .and_then(|response| RpcResponse::new(response).ok_or(RpcError::InvalidResponse)) - } + 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, + cookie: None, + method: method.to_owned(), + } + } + + pub fn set_cookie(&mut self, cookie: Cookie) { + self.cookie = Some(cookie); + } + + pub fn get_method(&self) -> String { + self.method.clone() + } + + pub fn send(self, url: &str) -> Result { + let client = Client::new(); + + let mut request = client.post(url); + request.body(self.body); + + if let Some(cookie) = self.cookie { + request.header(cookie); + } + + request.send() + .map_err(|e| RpcError::ConnectionError(e)) + .and_then(|response| RpcResponse::new(response).ok_or(RpcError::InvalidResponse)) + } } pub struct RpcResponse { - success: bool, - cookie: Cookie, - package: Package, + success: bool, + cookie: Cookie, + package: Package, } impl RpcResponse { - fn new(mut response: Response) -> Option { - 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; - let mut cookie = Cookie::new(); - - if let Some(&SetCookie(ref set_cookies)) = response.headers().get::() { - for set_cookie in set_cookies { - if let Ok(c) = cookie::Cookie::parse(set_cookie.to_owned()) { - cookie.append(c.name().to_owned(), c.value().to_owned()); - } - } - } - - 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; - } - } - } - - return Some(RpcResponse { - success, - cookie, - package, - }); - } - } - } - - None - } - - pub fn is_success(&self) -> bool { - self.success - } - - pub fn get_cookie(&self) -> Cookie { - self.cookie.clone() - } - - pub fn get_document(&self) -> Document { - self.package.as_document() - } + fn new(mut response: Response) -> Option { + 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; + let mut cookie = Cookie::new(); + + if let Some(&SetCookie(ref set_cookies)) = response.headers().get::() { + for set_cookie in set_cookies { + if let Ok(c) = cookie::Cookie::parse(set_cookie.to_owned()) { + cookie.append(c.name().to_owned(), c.value().to_owned()); + } + } + } + + 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; + } + } + } + + return Some(RpcResponse { + success, + cookie, + package, + }); + } + } + } + + None + } + + pub fn is_success(&self) -> bool { + self.success + } + + pub fn get_cookie(&self) -> Cookie { + self.cookie.clone() + } + + pub fn get_document(&self) -> Document { + self.package.as_document() + } }