Merge branch 'release/0.1.0'
This commit is contained in:
commit
866cbd33bc
10 changed files with 2292 additions and 0 deletions
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