Compare commits
2 Commits
dbdcf5a3ab
...
8e43820281
Author | SHA1 | Date |
---|---|---|
publicmatt | 8e43820281 | |
publicmatt | 0b128ee2e1 |
|
@ -0,0 +1,3 @@
|
||||||
|
GODADDY_DOMAIN="example.com"
|
||||||
|
GODADDY_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
GODADDY_SECRET="XXXXXXXXXXXXXXXXXXXXXX"
|
|
@ -1,10 +1,28 @@
|
||||||
|
deploy.sh
|
||||||
|
.env
|
||||||
|
|
||||||
# IntelliJ IDEA
|
# IntelliJ IDEA
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Rust
|
# Rust
|
||||||
/target
|
/target
|
||||||
|
.cargo/
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
ddns_ip
|
ddns_ip
|
||||||
records.json
|
records.json
|
||||||
|
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
# Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -14,6 +14,8 @@ log = { version = "0.4", features = ["std", "serde"] }
|
||||||
simple_logger = "1.16.0"
|
simple_logger = "1.16.0"
|
||||||
strfmt = "0.1.6"
|
strfmt = "0.1.6"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
clap = { version = "4.4.12", features = ["derive"]}
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct Auth {
|
||||||
|
#[clap(long)]
|
||||||
|
pub key: String,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
pub secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Auth {
|
||||||
|
pub fn as_header(&self) -> String {
|
||||||
|
let header = format!("sso-key {}:{}", self.key, self.secret);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(ValueEnum, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum RecordType {
|
||||||
|
A,
|
||||||
|
AAAA,
|
||||||
|
CNAME,
|
||||||
|
MX,
|
||||||
|
NS,
|
||||||
|
SOA,
|
||||||
|
SRV,
|
||||||
|
TXT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RecordType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
RecordType::A => "A",
|
||||||
|
RecordType::AAAA => "AAAA",
|
||||||
|
RecordType::CNAME => "CNAME",
|
||||||
|
RecordType::MX => "MX",
|
||||||
|
RecordType::NS => "NS",
|
||||||
|
RecordType::SOA => "SOA",
|
||||||
|
RecordType::SRV => "SRV",
|
||||||
|
RecordType::TXT => "TXT",
|
||||||
|
};
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{:<width$}",
|
||||||
|
text,
|
||||||
|
width = f.width().unwrap_or(0)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
pub struct DNSRecord {
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub domain: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub record_type: RecordType,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub data: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_ttl")]
|
||||||
|
pub ttl: u32,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub port: Option<u16>, // SRV Only.
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub priority: Option<u32>, // MX and SRV only.
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub protocol: Option<String>, // SRV only.
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub service: Option<String>, // SRV only.
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub weight: Option<u32>, // SRV only.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RecordType {
|
||||||
|
fn default() -> Self {
|
||||||
|
RecordType::A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_ttl() -> u32 {
|
||||||
|
600
|
||||||
|
}
|
|
@ -1,80 +0,0 @@
|
||||||
use std::fs::{create_dir, read_to_string, File};
|
|
||||||
use std::io::{Error, ErrorKind, Write};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::file_handler::path_handler::{
|
|
||||||
get_application_folder_path, get_ip_file_path, get_records_file_path,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod path_handler;
|
|
||||||
|
|
||||||
/// Sets up the application folder.
|
|
||||||
pub fn application_folder_setup() -> std::io::Result<()> {
|
|
||||||
let app_folder_path = get_application_folder_path();
|
|
||||||
|
|
||||||
if app_folder_path.is_none() {
|
|
||||||
return Err(Error::from(ErrorKind::NotFound));
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = app_folder_path.unwrap();
|
|
||||||
|
|
||||||
if path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
create_dir(&path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the contents of the RECORDS_FILE.
|
|
||||||
pub fn get_records_file() -> String {
|
|
||||||
let path = get_records_file_path().expect("Couldn't get RECORDS_FILE path.");
|
|
||||||
|
|
||||||
read_file(&path).expect(&format!(
|
|
||||||
"{} does not exist. Please create it.",
|
|
||||||
path.display()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the contents of the IP_FILE.
|
|
||||||
pub fn get_ip_file() -> Option<String> {
|
|
||||||
let path = get_ip_file_path().expect("Couldn't get IP_FILE path.");
|
|
||||||
|
|
||||||
read_file(&path).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores the current IP value in the FILE_NAME.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `content` - A &[u8] holding the current IP value.
|
|
||||||
pub fn set_ip_file(content: &[u8]) -> () {
|
|
||||||
let path = get_ip_file_path().expect("Couldn't get IP_FILE path.");
|
|
||||||
|
|
||||||
write_file(&path, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads a file and returns its contents as String if Ok().
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - A &Path holding the path of the file to be read.
|
|
||||||
fn read_file(path: &Path) -> std::io::Result<String> {
|
|
||||||
read_to_string(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a file.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - A &Path holding the path of the file to be written.
|
|
||||||
///
|
|
||||||
/// * `content` - A &[u8] holding the info that will be written to the file.
|
|
||||||
fn write_file(path: &Path, content: &[u8]) -> () {
|
|
||||||
let display = &path.display();
|
|
||||||
|
|
||||||
let mut file =
|
|
||||||
File::create(path).expect(&format!("Error opening or creating file: {}", display));
|
|
||||||
|
|
||||||
file.write_all(content)
|
|
||||||
.expect(&format!("Error writing file: {}", display))
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
const APP_DIR: &'static str = ".godaddy-ddns";
|
|
||||||
const IP_FILE_NAME: &'static str = "ddns_ip";
|
|
||||||
const RECORDS_FILE_NAME: &'static str = "records.json";
|
|
||||||
|
|
||||||
/// Gets the application folder path and returns it if found.
|
|
||||||
pub fn get_application_folder_path() -> Option<PathBuf> {
|
|
||||||
let home = dirs::home_dir();
|
|
||||||
|
|
||||||
if home.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(PathBuf::from(format!(
|
|
||||||
"{}/{}",
|
|
||||||
home.unwrap().display(),
|
|
||||||
APP_DIR
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the IP_FILE path and returns it if found.
|
|
||||||
pub fn get_ip_file_path() -> Option<PathBuf> {
|
|
||||||
let app_folder = get_application_folder_path();
|
|
||||||
|
|
||||||
if app_folder.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(PathBuf::from(format!(
|
|
||||||
"{}/{}",
|
|
||||||
app_folder.unwrap().display(),
|
|
||||||
IP_FILE_NAME
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the RECORDS_FILE path and returns it if found.
|
|
||||||
pub fn get_records_file_path() -> Option<PathBuf> {
|
|
||||||
let app_folder = get_application_folder_path();
|
|
||||||
|
|
||||||
if app_folder.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(PathBuf::from(format!(
|
|
||||||
"{}/{}",
|
|
||||||
app_folder.unwrap().display(),
|
|
||||||
RECORDS_FILE_NAME
|
|
||||||
)))
|
|
||||||
}
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::dns::DNSRecord;
|
||||||
|
|
||||||
|
pub enum Api {
|
||||||
|
Patch(DNSRecord),
|
||||||
|
Delete(DNSRecord),
|
||||||
|
Get(DNSRecord),
|
||||||
|
List(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ResponseError {
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub fields: Vec<ResponseField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ResponseField {
|
||||||
|
#[serde(default)]
|
||||||
|
pub code: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub message: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub path: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(rename = "pathRelated")]
|
||||||
|
pub path_related: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Api {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Api::Patch(record) => write!(
|
||||||
|
f,
|
||||||
|
"https://api.godaddy.com/v1/domains/{domain}/records",
|
||||||
|
domain = record.domain,
|
||||||
|
),
|
||||||
|
Api::Delete(record) => write!(
|
||||||
|
f,
|
||||||
|
"https://api.godaddy.com/v1/domains/{domain}/records/{record_type}/{name}",
|
||||||
|
domain = record.domain,
|
||||||
|
record_type = record.record_type,
|
||||||
|
name = record.name
|
||||||
|
),
|
||||||
|
Api::Get(record) => write!(
|
||||||
|
f,
|
||||||
|
"https://api.godaddy.com/v1/domains/{domain}/records/{record_type}/{name}",
|
||||||
|
domain = record.domain,
|
||||||
|
record_type = record.record_type,
|
||||||
|
name = record.name
|
||||||
|
),
|
||||||
|
Api::List(domain) => write!(
|
||||||
|
f,
|
||||||
|
"https://api.godaddy.com/v1/domains/{domain}/records",
|
||||||
|
domain = domain,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
use crate::dns::DNSRecord;
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
|
|
||||||
|
use log::{debug, info};
|
||||||
|
|
||||||
|
use crate::auth::Auth;
|
||||||
|
use api::{Api, ResponseError};
|
||||||
|
use reqwest::Response;
|
||||||
|
|
||||||
|
/// Sends a put request to the GoDaddy API to update a DNS record.
|
||||||
|
pub async fn update_record(record: DNSRecord, auth: &Auth) -> () {
|
||||||
|
let api: Api = Api::Patch(record.clone());
|
||||||
|
|
||||||
|
let body = vec![record.clone()];
|
||||||
|
debug!("{:?}", body);
|
||||||
|
|
||||||
|
let header = auth.as_header();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let req = client
|
||||||
|
.patch(api.to_string())
|
||||||
|
.json(&body)
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("authorization", &header);
|
||||||
|
debug!("{:?}", api.to_string());
|
||||||
|
|
||||||
|
let response = req.send().await.expect("Error updating records.");
|
||||||
|
parse_response(response, record).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_record(record: DNSRecord, auth: &Auth) -> () {
|
||||||
|
let api: Api = Api::Delete(record.clone());
|
||||||
|
|
||||||
|
let header = auth.as_header();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let req = client
|
||||||
|
.delete(api.to_string())
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.header("authorization", &header);
|
||||||
|
|
||||||
|
let response = req.send().await.expect("Error deleting record.");
|
||||||
|
parse_response(response, record).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_response(response: Response, record: DNSRecord) -> () {
|
||||||
|
match response.status() {
|
||||||
|
s if s.is_success() => {
|
||||||
|
info!("success: {:?}", record);
|
||||||
|
}
|
||||||
|
s if s.is_client_error() => {
|
||||||
|
let body = response.text().await.unwrap();
|
||||||
|
match serde_json::from_str::<ResponseError>(&body) {
|
||||||
|
Ok(json) => {
|
||||||
|
info!("client error [{}]: {}\n{:?}", s, json.message, json.fields);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to parse JSON: {:?}", e);
|
||||||
|
eprintln!("Raw response body: {:?}", body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
s => {
|
||||||
|
let body = response.text().await.unwrap();
|
||||||
|
match serde_json::from_str::<ResponseError>(&body) {
|
||||||
|
Ok(json) => {
|
||||||
|
info!("client error [{}]: {}\n{:?}", s, json.message, json.fields);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to parse JSON: {:?}", e);
|
||||||
|
eprintln!("Raw response body: {:?}", body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_record(record: &DNSRecord, auth: &Auth) -> Vec<DNSRecord> {
|
||||||
|
let api: Api = Api::Get(record.clone());
|
||||||
|
|
||||||
|
let header = auth.as_header();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let req = client
|
||||||
|
.get(api.to_string())
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.header("authorization", &header);
|
||||||
|
|
||||||
|
let response = req.send().await.expect("Error listing records.");
|
||||||
|
if response.status().is_success() {
|
||||||
|
let data = &response.text().await.expect("Error reading response text");
|
||||||
|
let record: Vec<DNSRecord> = serde_json::from_str(data).expect("Error parsing response");
|
||||||
|
return record;
|
||||||
|
} else {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_records(domain: &str, auth: &Auth) -> () {
|
||||||
|
let api: Api = Api::List(domain.to_string());
|
||||||
|
|
||||||
|
let header = auth.as_header();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let req = client
|
||||||
|
.get(api.to_string())
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.header("authorization", &header);
|
||||||
|
|
||||||
|
let response = req.send().await.expect("Error listing records.");
|
||||||
|
if response.status().is_success() {
|
||||||
|
let body = response.text().await.expect("Error reading response text");
|
||||||
|
let records: Vec<DNSRecord> = serde_json::from_str(&body).unwrap();
|
||||||
|
// .expect("Error parsing records into object");
|
||||||
|
println!("{:<5} {:<25} {:<30}", "Type", "Name", "Value"); // Header
|
||||||
|
for record in records {
|
||||||
|
println!(
|
||||||
|
"{:<5} {:<25} {:<30}",
|
||||||
|
record.record_type, record.name, record.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Request failed with status: {}", response.status());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct IpResponse {
|
||||||
|
origin: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEBSITE_URL: &'static str = "https://httpbin.org/ip";
|
||||||
|
|
||||||
|
/// Checks the current WAN IP.
|
||||||
|
///
|
||||||
|
/// Connects to WEBSITE_URL and retrieves the current WAN IP value.
|
||||||
|
async fn check_current_ip() -> Result<IpAddr, Box<dyn Error>> {
|
||||||
|
const WEBSITE_URL: &'static str = "https://httpbin.org/ip";
|
||||||
|
let resp = reqwest::get(WEBSITE_URL)
|
||||||
|
.await?
|
||||||
|
.json::<IpResponse>()
|
||||||
|
.await?;
|
||||||
|
let ip = resp.origin.parse::<IpAddr>()?;
|
||||||
|
|
||||||
|
Ok(ip)
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct IP {
|
|
||||||
pub origin: String,
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
use log::debug;
|
|
||||||
|
|
||||||
use crate::file_handler::{get_ip_file, set_ip_file};
|
|
||||||
use crate::ip_handler::ip::IP;
|
|
||||||
|
|
||||||
mod ip;
|
|
||||||
|
|
||||||
const WEBSITE_URL: &'static str = "https://httpbin.org/ip";
|
|
||||||
|
|
||||||
/// Returns an Option holding the current IP address if the value has changed, otherwise returns None.
|
|
||||||
pub async fn get_ip_to_publish() -> Option<String> {
|
|
||||||
let previous_ip = match check_previous_ip() {
|
|
||||||
Some(x) => x,
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let current_ip = check_current_ip()
|
|
||||||
.await
|
|
||||||
.expect("Error getting the current IP.");
|
|
||||||
|
|
||||||
debug!("Current IP: {}, Previous IP: {}", current_ip, previous_ip);
|
|
||||||
|
|
||||||
if current_ip.eq(&previous_ip) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(current_ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the current WAN IP.
|
|
||||||
///
|
|
||||||
/// Connects to WEBSITE_URL and retrieves the current WAN IP value.
|
|
||||||
async fn check_current_ip() -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
let resp = reqwest::get(WEBSITE_URL).await?.json::<IP>().await?;
|
|
||||||
|
|
||||||
set_ip_file((&resp.origin).as_ref());
|
|
||||||
|
|
||||||
Ok(resp.origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the current IP value from the FILE_NAME and returns it.
|
|
||||||
fn check_previous_ip() -> Option<String> {
|
|
||||||
get_ip_file()
|
|
||||||
}
|
|
116
src/main.rs
116
src/main.rs
|
@ -1,12 +1,80 @@
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use dotenv::dotenv;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use simple_logger::SimpleLogger;
|
use simple_logger::SimpleLogger;
|
||||||
|
|
||||||
mod file_handler;
|
mod auth;
|
||||||
mod records_handler;
|
mod dns;
|
||||||
mod ip_handler;
|
mod godaddy;
|
||||||
|
|
||||||
|
use auth::Auth;
|
||||||
|
use dns::{DNSRecord, RecordType};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(
|
||||||
|
author = "publicmatt",
|
||||||
|
version = "1.0",
|
||||||
|
about = "add/update/delete godaddy dns records"
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: SubCommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum SubCommands {
|
||||||
|
#[clap(about = "list dns record names/types")]
|
||||||
|
List(ListCmd),
|
||||||
|
|
||||||
|
#[clap(about = "delete dns record name/type")]
|
||||||
|
Delete(DeleteCmd),
|
||||||
|
|
||||||
|
#[clap(about = "add/update dns record name/type")]
|
||||||
|
Update(UpdateCmd),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct UpdateCmd {
|
||||||
|
#[clap(long)]
|
||||||
|
domain: String,
|
||||||
|
|
||||||
|
/// Subdomain
|
||||||
|
#[clap(long)]
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
ttl: Option<u32>,
|
||||||
|
|
||||||
|
#[clap(long, default_value_t = RecordType::A)]
|
||||||
|
record_type: RecordType,
|
||||||
|
|
||||||
|
/// IP Address or data for the record
|
||||||
|
#[clap(long)]
|
||||||
|
data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct DeleteCmd {
|
||||||
|
#[clap(long)]
|
||||||
|
domain: String,
|
||||||
|
|
||||||
|
/// Subdomain
|
||||||
|
#[clap(long)]
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[clap(long, default_value_t = RecordType::A)]
|
||||||
|
record_type: RecordType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct ListCmd {
|
||||||
|
#[clap(long)]
|
||||||
|
domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
@ -17,11 +85,43 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.init()
|
.init()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
file_handler::application_folder_setup().expect("Error setting up application folder.");
|
dotenv().ok();
|
||||||
|
|
||||||
let domain = env::var("DOMAIN").expect("You need to set DOMAIN env variable first.");
|
let auth: Auth = Auth {
|
||||||
let key = env::var("KEY").expect("You need to set KEY env variable first.");
|
key: env::var("GODADDY_KEY").expect("You need to set GODADDY_KEY env variable first."),
|
||||||
let secret = env::var("SECRET").expect("You need to set SECRET env variable first.");
|
secret: env::var("GODADDY_SECRET")
|
||||||
|
.expect("You need to set GODADDY_SECRET env variable first."),
|
||||||
|
};
|
||||||
|
|
||||||
records_handler::update(&domain, &key, &secret).await
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.subcommand {
|
||||||
|
SubCommands::Update(args) => {
|
||||||
|
let record = DNSRecord {
|
||||||
|
domain: args.domain,
|
||||||
|
name: args.name,
|
||||||
|
record_type: args.record_type,
|
||||||
|
data: args.data,
|
||||||
|
ttl: 600,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
godaddy::update_record(record, &auth).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
SubCommands::Delete(args) => {
|
||||||
|
let record = DNSRecord {
|
||||||
|
domain: args.domain,
|
||||||
|
name: args.name,
|
||||||
|
record_type: args.record_type,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
godaddy::delete_record(record, &auth).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
SubCommands::List(args) => {
|
||||||
|
let domain = args.domain;
|
||||||
|
godaddy::list_records(&domain, &auth).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct DNSRecordsHolder {
|
|
||||||
pub records: Vec<DNSRecord>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct DNSRecord {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub record_type: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub data: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub port: Option<u16>, // SRV Only.
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub priority: Option<u32>, // MX and SRV only.
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub protocol: Option<String>, // SRV only.
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub service: Option<String>, // SRV only.
|
|
||||||
|
|
||||||
pub ttl: u32,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub interpolate: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub weight: Option<u32>, // SRV only.
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use log::{debug, info};
|
|
||||||
use strfmt::strfmt;
|
|
||||||
|
|
||||||
use crate::file_handler::get_records_file;
|
|
||||||
use crate::records_handler::dns_record::{DNSRecord, DNSRecordsHolder};
|
|
||||||
use crate::ip_handler::get_ip_to_publish;
|
|
||||||
|
|
||||||
mod dns_record;
|
|
||||||
|
|
||||||
/// Updates the DNS records if the IP has changed and returns the result of the execution.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `domain` - A &str holding the domain to update.
|
|
||||||
///
|
|
||||||
/// * `key` - A &str holding the GoDaddy developer key.
|
|
||||||
///
|
|
||||||
/// * `secret` - A &str holding the GoDaddy developer secret.
|
|
||||||
pub async fn update(domain: &str, key: &str, secret: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let records = get_records();
|
|
||||||
|
|
||||||
info!("Checking if the IP has changed.");
|
|
||||||
let new_ip = get_ip_to_publish().await;
|
|
||||||
|
|
||||||
// There's no need to do anything here. So we stop the execution.
|
|
||||||
if new_ip.is_none() {
|
|
||||||
info!("The IP hasn't changed. Let's stop the execution here.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("The IP has changed. Let's update the DNS records.");
|
|
||||||
for record in records {
|
|
||||||
debug!("{:?}", record);
|
|
||||||
update_record(record, &new_ip.clone().unwrap(), domain, key, secret).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a vector of DNSRecord from RECORDS_FILE_NAME and returns it.
|
|
||||||
fn get_records() -> Vec<DNSRecord> {
|
|
||||||
let content = get_records_file();
|
|
||||||
|
|
||||||
let base: DNSRecordsHolder =
|
|
||||||
serde_json::from_str(&content).expect("Failed to deserialize JSON");
|
|
||||||
|
|
||||||
base.records
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a put request to the GoDaddy API to update a DNS record.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `record` - A DNSRecord holding the record to update.
|
|
||||||
///
|
|
||||||
/// * `value` - A &str holding the current WAN ip.
|
|
||||||
///
|
|
||||||
/// * `domain` - A &str holding the domain to update.
|
|
||||||
///
|
|
||||||
/// * `key` - A &str holding the GoDaddy developer key.
|
|
||||||
///
|
|
||||||
/// * `secret` - A &str holding the GoDaddy developer secret.
|
|
||||||
async fn update_record(
|
|
||||||
record: DNSRecord,
|
|
||||||
value: &str,
|
|
||||||
domain: &str,
|
|
||||||
key: &str,
|
|
||||||
secret: &str,
|
|
||||||
) -> () {
|
|
||||||
let url = format!(
|
|
||||||
"https://api.godaddy.com/v1/domains/{domain}/records/{record_type}/{name}",
|
|
||||||
domain = domain,
|
|
||||||
record_type = record.record_type.unwrap(),
|
|
||||||
name = record.name.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let data = match &record.data {
|
|
||||||
Some(x) => {
|
|
||||||
if record.interpolate.is_some() && record.interpolate.unwrap() == true {
|
|
||||||
let mut vars = HashMap::new();
|
|
||||||
vars.insert("ip".to_string(), value);
|
|
||||||
|
|
||||||
strfmt(x, &vars).expect("Error interpolating {ip} from data.")
|
|
||||||
} else {
|
|
||||||
String::from(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => String::from(value),
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = vec![DNSRecord {
|
|
||||||
name: None,
|
|
||||||
record_type: None,
|
|
||||||
data: Some(data),
|
|
||||||
port: record.port,
|
|
||||||
priority: record.priority,
|
|
||||||
protocol: record.protocol,
|
|
||||||
service: record.service,
|
|
||||||
ttl: record.ttl,
|
|
||||||
interpolate: None,
|
|
||||||
weight: record.weight,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let header = format!("sso-key {}:{}", key, secret);
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
let req = client
|
|
||||||
.put(url)
|
|
||||||
.json(&body)
|
|
||||||
.header("accept", "application/json")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.header("authorization", &header);
|
|
||||||
|
|
||||||
req.send().await.expect("Error updating record.");
|
|
||||||
}
|
|
Loading…
Reference in New Issue