Merge branch 'release/0.1.0'
This commit is contained in:
commit
866cbd33bc
22
.github/workflows/build.yml
vendored
Normal file
22
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
on: push
|
||||||
|
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_test:
|
||||||
|
name: Build cloudflare-ddns
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout latest
|
||||||
|
uses: actions/checkout@master
|
||||||
|
- name: Install nightly toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
- name: Cargo build
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
command: build
|
||||||
|
arguments: --release
|
29
.github/workflows/publish.yml
vendored
Normal file
29
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
name: Publish to crates.io
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_test:
|
||||||
|
name: Publish cloudflare-ddns
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout latest master
|
||||||
|
uses: actions/checkout@master
|
||||||
|
- name: Install nightly toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
- name: Login to crates.io
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
command: login ${{ secrets.CRATES_TOKEN }}
|
||||||
|
- name: Publish to crates.io
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
command: publish
|
42
.github/workflows/release.yml
vendored
Normal file
42
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
name: Create a GitHub release
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@master
|
||||||
|
- name: Install nightly toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
- name: Cargo build
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
command: build
|
||||||
|
arguments: --release
|
||||||
|
- name: Extract version
|
||||||
|
id: version
|
||||||
|
uses: actions/github-script@0.2.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
return context.payload.ref.replace(/\/refs\/tags\//, '');
|
||||||
|
- name: Archive
|
||||||
|
run: tar cfJ cloudflare-ddns-${{ steps.version.outputs.result }}.tar.xz target/cloudflare-ddns
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
body: |
|
||||||
|
cloudflare-ddns release ${{ steps.version.outputs.result }}
|
||||||
|
files: |
|
||||||
|
cloudflare-ddns-${{ steps.version.outputs.result }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
1957
Cargo.lock
generated
Normal file
1957
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "cloudflare-ddns"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Rostislav Raykov <z@zbrox.org>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "A simple CLI tool to use Cloudflare's free DDNS service"
|
||||||
|
repository = "git@github.com:zbrox/cloudflare-ddns.git"
|
||||||
|
homepage = "https://github.com/zbrox/cloudflare-ddns"
|
||||||
|
keywords = ["cloudflare", "ddns", "cli"]
|
||||||
|
categories = ["command-line-utilities"]
|
||||||
|
license = "MIT"
|
||||||
|
documentation = "https://github.com/zbrox/cloudflare-ddns"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[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"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Rostislav Raykov <z@zbrox.org>
|
||||||
|
|
||||||
|
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.
|
19
src/file.rs
Normal file
19
src/file.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use failure::Error;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
pub fn read_cache_file(path: &PathBuf) -> Result<String, Error> {
|
||||||
|
let mut file = File::open(&path)?;
|
||||||
|
let mut s = String::new();
|
||||||
|
file.read_to_string(&mut s)?;
|
||||||
|
|
||||||
|
Ok(s.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_cache_file(path: &PathBuf, ip: &str) -> Result<(), Error> {
|
||||||
|
let mut file = File::create(&path)?;
|
||||||
|
file.write_all(ip.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
72
src/main.rs
Normal file
72
src/main.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
mod network;
|
||||||
|
mod file;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use quicli::prelude::*;
|
||||||
|
use structopt::StructOpt;
|
||||||
|
use human_panic::{setup_panic};
|
||||||
|
use network::{get_zone_identifier, get_dns_record_id, get_current_ip, update_ddns};
|
||||||
|
use file::{read_cache_file, write_cache_file};
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
/// Inform Cloudflare's DDNS service of the current IP address for your domain
|
||||||
|
struct Cli {
|
||||||
|
/// Your Cloudflare login email
|
||||||
|
#[structopt(long = "email", short = "e")]
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
/// The auth key you need to generate in your Cloudflare profile
|
||||||
|
#[structopt(long = "key", short = "k")]
|
||||||
|
auth_key: String,
|
||||||
|
|
||||||
|
/// The zone in which your domain is (usually that is your domain without the subdomain)
|
||||||
|
#[structopt(long = "zone", short = "z")]
|
||||||
|
zone: String,
|
||||||
|
|
||||||
|
/// The domain for which you want to report the current IP address
|
||||||
|
#[structopt(long = "domain", short = "d")]
|
||||||
|
domain: String,
|
||||||
|
|
||||||
|
/// Cache file for previously reported IP address (if skipped the IP will be reported on every execution)
|
||||||
|
#[structopt(long = "cache", short = "c")]
|
||||||
|
cache: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> CliResult {
|
||||||
|
setup_panic!();
|
||||||
|
|
||||||
|
let args = Cli::from_args();
|
||||||
|
let should_use_cache = args.cache.is_some();
|
||||||
|
|
||||||
|
let cached_ip: Option<String> = match args.cache.clone() {
|
||||||
|
Some(v) => {
|
||||||
|
if v.exists() {
|
||||||
|
Some(read_cache_file(&v.clone())?)
|
||||||
|
} else {
|
||||||
|
Some("0.0.0.0".to_owned())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let current_ip = get_current_ip()?;
|
||||||
|
if cached_ip.is_some() && current_ip == cached_ip.unwrap() {
|
||||||
|
println!("IP is unchanged. Exiting...");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_use_cache {
|
||||||
|
println!("Saving current IP {} to cache file {:?}...", ¤t_ip, &args.cache.clone().unwrap());
|
||||||
|
write_cache_file(&args.cache.unwrap(), ¤t_ip)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zone_id = get_zone_identifier(&args.zone, &args.email, &args.auth_key)?;
|
||||||
|
let record_id = get_dns_record_id(&zone_id, &args.domain, &args.email, &args.auth_key)?;
|
||||||
|
|
||||||
|
update_ddns(¤t_ip, &args.domain, &zone_id, &record_id, &args.email, &args.auth_key)?;
|
||||||
|
|
||||||
|
println!("Successfully updated the A record for {} to {}", &args.domain, ¤t_ip);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
104
src/network.rs
Normal file
104
src/network.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use failure::{Error, format_err};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct CloudflareListResponse {
|
||||||
|
success: bool,
|
||||||
|
errors: Vec<String>,
|
||||||
|
result: Vec<ObjectWithId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct CloudflareUpdateResponse {
|
||||||
|
success: bool,
|
||||||
|
errors: Vec<String>,
|
||||||
|
result: ObjectWithId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, PartialEq)]
|
||||||
|
struct ObjectWithId {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct UpdateIpData {
|
||||||
|
id: String,
|
||||||
|
r#type: String,
|
||||||
|
name: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_zone_identifier(zone: &str, email: &str, key: &str) -> Result<String, Error> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = format!("https://api.cloudflare.com/client/v4/zones?name={}", zone);
|
||||||
|
let response: CloudflareListResponse = 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.errors.iter().map(|s| format!("{}\n", s.to_owned())).collect();
|
||||||
|
return Err(format_err!("API Error: {}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.result[0].id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dns_record_id(zone_id: &str, domain: &str, email: &str, key: &str) -> Result<String, Error> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records?name={}", zone_id, domain);
|
||||||
|
let response: CloudflareListResponse = 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.errors.iter().map(|s| format!("{}\n", s.to_owned())).collect();
|
||||||
|
return Err(format_err!("API Error: {}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.result[0].id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_ip() -> Result<String, Error> {
|
||||||
|
Ok(reqwest::Client::new()
|
||||||
|
.get("http://ipv4.icanhazip.com")
|
||||||
|
.send()?
|
||||||
|
.text()?
|
||||||
|
.trim()
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_ddns(ip: &str, domain: &str, zone_id: &str, record_id: &str, email: &str, key: &str) -> Result<(), Error> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", zone_id, record_id);
|
||||||
|
|
||||||
|
let update_data = UpdateIpData {
|
||||||
|
id: zone_id.to_owned(),
|
||||||
|
r#type: "A".to_owned(),
|
||||||
|
name: domain.to_owned(),
|
||||||
|
content: ip.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: CloudflareUpdateResponse = client
|
||||||
|
.put(&url)
|
||||||
|
.header("X-Auth-Email", email)
|
||||||
|
.header("X-Auth-Key", key)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&update_data)
|
||||||
|
.send()?
|
||||||
|
.json()?;
|
||||||
|
|
||||||
|
if !response.success {
|
||||||
|
let err: String = response.errors.iter().map(|s| format!("{}\n", s.to_owned())).collect();
|
||||||
|
return Err(format_err!("Unsuccessful update of DNS record: {}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue