Merge branch 'release/0.2.1'

This commit is contained in:
Rostislav Raykov 2020-03-22 23:31:53 +01:00
commit 2920328093
7 changed files with 775 additions and 1019 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
**/*.rs.bk
.envrc

50
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,50 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.1",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug 'cloudflare-ddns'",
"cargo": {
"args": [
"build",
"--bin=cloudflare-ddns",
"--package=cloudflare-ddns"
],
"filter": {
"name": "cloudflare-ddns",
"kind": "bin"
}
},
"args": [
"--key=${env:DDNS_AUTH_KEY}",
"--domain=${env:DOMAIN}",
"--email=${env:EMAIL}",
"--zone=${env:ZONE}"
],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in 'cloudflare-ddns'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=cloudflare-ddns",
"--package=cloudflare-ddns"
],
"filter": {
"name": "cloudflare-ddns",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

1592
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "cloudflare-ddns"
version = "0.2.0"
version = "0.2.1"
authors = ["Rostislav Raykov <z@zbrox.org>"]
edition = "2018"
description = "A simple CLI tool to use Cloudflare's free DDNS service"
@ -14,11 +14,11 @@ readme = "README.md"
[dependencies]
quicli = "0.4"
structopt = "0.3.1"
reqwest="0.9.20"
serde = "1.0.101"
serde_json = "1.0.40"
exitcode = "1.1.2"
human-panic = "1.0.1"
failure = "0.1.5"
toml = "0.5.3"
structopt = "0.3.12"
reqwest= { version = "0.10.4", features = ["blocking", "json"] }
serde = "1.0.105"
serde_json = "1.0.48"
toml = "0.5.6"
human-panic = "1.0.2"
serde_derive = "1.0.105"
anyhow = "1.0.27"

View file

@ -1,12 +1,18 @@
use failure::Error;
use quicli::fs::{write_to_file, read_file};
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use anyhow::Result;
pub fn read_cache_file(path: &PathBuf) -> Result<String, Error> {
Ok(read_file(path)?)
pub fn read_file(path: &PathBuf) -> Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
pub fn write_cache_file(path: &PathBuf, ip: &str) -> Result<(), Error> {
write_to_file(path, ip)?;
pub fn write_file(path: &PathBuf, ip: &str) -> Result<()> {
let mut file = File::create(path)?;
file.write_all(ip.as_bytes())?;
Ok(())
}

View file

@ -1,13 +1,13 @@
mod file;
mod network;
use file::{read_cache_file, write_cache_file};
use file::{read_file, write_file};
use human_panic::setup_panic;
use network::{get_current_ip, get_dns_record_id, get_zone_identifier, update_ddns};
use quicli::fs::read_file;
use quicli::prelude::*;
use std::path::PathBuf;
use structopt::StructOpt;
use anyhow::{Context, Result};
#[derive(Deserialize)]
struct Config {
@ -45,7 +45,7 @@ struct Cli {
cache: Option<PathBuf>,
}
fn main() -> CliResult {
fn main() -> Result<()> {
setup_panic!();
let args = Cli::from_args();
@ -53,7 +53,7 @@ fn main() -> CliResult {
let cached_ip: Option<String> = match args.cache.clone() {
Some(v) => {
if v.exists() {
Some(read_cache_file(&v.clone())?)
Some(read_file(&v.clone()).context("Could not read cache file")?)
} else {
Some("0.0.0.0".to_owned())
}
@ -73,12 +73,12 @@ fn main() -> CliResult {
&current_ip,
&args.cache.clone().unwrap()
);
write_cache_file(&args.cache.unwrap(), &current_ip)?;
write_file(&args.cache.unwrap(), &current_ip)?;
}
let (email, auth_key, zone, domain) = match args.config {
Some(c) => {
let config_str = read_file(c)?;
let config_str = read_file(&c)?;
let config: Config = toml::from_str(&config_str)?;
(config.email, config.auth_key, config.zone, config.domain)
}
@ -106,9 +106,9 @@ fn update(
auth_key: &str,
zone: &str,
domain: &str,
) -> Result<(), Error> {
let zone_id = get_zone_identifier(&zone, &email, &auth_key)?;
let record_id = get_dns_record_id(&zone_id, &domain, &email, &auth_key)?;
) -> Result<()> {
let zone_id = get_zone_identifier(&zone, &email, &auth_key).context("Error getting the zone identifier")?;
let record_id = get_dns_record_id(&zone_id, &domain, &email, &auth_key).context("Error getting the DNS record ID")?;
update_ddns(
&current_ip,
@ -117,7 +117,7 @@ fn update(
&record_id,
&email,
&auth_key,
)?;
).context("Error updating the DNS record")?;
Ok(())
}

View file

@ -1,11 +1,11 @@
use failure::{format_err, Error};
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
use anyhow::{anyhow};
#[derive(Deserialize, Debug)]
struct CloudflareListResponse {
success: bool,
errors: Vec<String>,
result: Vec<ObjectWithId>,
result: Option<Vec<ObjectWithId>>,
}
#[derive(Deserialize, Debug)]
@ -28,26 +28,41 @@ struct UpdateIpData {
content: String,
}
pub fn get_zone_identifier(zone: &str, email: &str, key: &str) -> Result<String, Error> {
let client = reqwest::Client::new();
pub fn get_zone_identifier(zone: &str, email: &str, key: &str) -> anyhow::Result<String> {
let client = reqwest::blocking::Client::new();
let url = format!("https://api.cloudflare.com/client/v4/zones?name={}", zone);
let response: CloudflareListResponse = client
let response = client
.get(&url)
.header("X-Auth-Email", email)
.header("X-Auth-Key", key)
.header("Content-Type", "application/json")
.send()?
.send()?;
if response.status() != 200 {
return Err(anyhow!("API Error: HTTP {}", response.status()));
}
let body: CloudflareListResponse = response
.json()?;
if !response.success {
let err: String = response
if !body.success {
let err: String = body
.errors
.iter()
.map(|s| format!("{}\n", s.to_owned()))
.collect();
return Err(format_err!("API Error: {}", err));
return Err(anyhow!("API Error: {}", err));
}
Ok(response.result[0].id.clone())
let zones = match body.result {
Some(v) => v,
None => {
return Err(anyhow!("No zones returned"))
},
};
Ok(zones[0].id.clone())
}
pub fn get_dns_record_id(
@ -55,32 +70,46 @@ pub fn get_dns_record_id(
domain: &str,
email: &str,
key: &str,
) -> Result<String, Error> {
let client = reqwest::Client::new();
) -> anyhow::Result<String> {
let client = reqwest::blocking::Client::new();
let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?name={}",
zone_id, domain
);
let response: CloudflareListResponse = client
let response = client
.get(&url)
.header("X-Auth-Email", email)
.header("X-Auth-Key", key)
.header("Content-Type", "application/json")
.send()?
.json()?;
if !response.success {
let err: String = response
.send()?;
if response.status() != 200 {
return Err(anyhow!("API Error: HTTP {}", response.status()));
}
let body: CloudflareListResponse = response.json()?;
if !body.success {
let err: String = body
.errors
.iter()
.map(|s| format!("{}\n", s.to_owned()))
.collect();
return Err(format_err!("API Error: {}", err));
return Err(anyhow!("API Error: {}", err));
}
let id = match response.result.first() {
let records = match body.result {
Some(v) => v,
None => {
return Err(anyhow!("No DNS records returned"))
},
};
let id = match records.first() {
Some(v) => v.id.clone(),
None => {
return Err(format_err!(
return Err(anyhow!(
"Unexpected API result for DNS record. Check if you passed the right options."
))
}
@ -89,8 +118,8 @@ pub fn get_dns_record_id(
Ok(id)
}
pub fn get_current_ip() -> Result<String, Error> {
Ok(reqwest::Client::new()
pub fn get_current_ip() -> anyhow::Result<String> {
Ok(reqwest::blocking::Client::new()
.get("http://ipv4.icanhazip.com")
.send()?
.text()?
@ -105,8 +134,8 @@ pub fn update_ddns(
record_id: &str,
email: &str,
key: &str,
) -> Result<(), Error> {
let client = reqwest::Client::new();
) -> anyhow::Result<()> {
let client = reqwest::blocking::Client::new();
let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}",
zone_id, record_id
@ -119,22 +148,28 @@ pub fn update_ddns(
content: ip.to_owned(),
};
let response: CloudflareUpdateResponse = client
let response = client
.put(&url)
.header("X-Auth-Email", email)
.header("X-Auth-Key", key)
.header("Content-Type", "application/json")
.json(&update_data)
.send()?
.send()?;
if response.status() != 200 {
return Err(anyhow!("API Error: HTTP {}", response.status()));
}
let body: CloudflareUpdateResponse = response
.json()?;
if !response.success {
let err: String = response
if !body.success {
let err: String = body
.errors
.iter()
.map(|s| format!("{}\n", s.to_owned()))
.collect();
return Err(format_err!("Unsuccessful update of DNS record: {}", err));
return Err(anyhow!("Unsuccessful update of DNS record: {}", err));
}
Ok(())