Compare commits
No commits in common. "8e43820281b0bf73bcbace2cf7d8689a6970dcbe" and "dbdcf5a3ab64872bf764c00758d244f44baab90b" have entirely different histories.
8e43820281
...
dbdcf5a3ab
|
@ -1,3 +0,0 @@
|
|||
GODADDY_DOMAIN="example.com"
|
||||
GODADDY_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
GODADDY_SECRET="XXXXXXXXXXXXXXXXXXXXXX"
|
|
@ -1,28 +1,10 @@
|
|||
deploy.sh
|
||||
.env
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# Rust
|
||||
/target
|
||||
.cargo/
|
||||
|
||||
# Project
|
||||
ddns_ip
|
||||
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,8 +14,6 @@ log = { version = "0.4", features = ["std", "serde"] }
|
|||
simple_logger = "1.16.0"
|
||||
strfmt = "0.1.6"
|
||||
dirs = "4.0.0"
|
||||
dotenv = "0.15.0"
|
||||
clap = { version = "4.4.12", features = ["derive"]}
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
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))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
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
|
||||
)))
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
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,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
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());
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct IP {
|
||||
pub origin: String,
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
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,80 +1,12 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use log::LevelFilter;
|
||||
use simple_logger::SimpleLogger;
|
||||
|
||||
mod auth;
|
||||
mod dns;
|
||||
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,
|
||||
}
|
||||
mod file_handler;
|
||||
mod records_handler;
|
||||
mod ip_handler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
@ -85,43 +17,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
.init()
|
||||
.unwrap();
|
||||
|
||||
dotenv().ok();
|
||||
file_handler::application_folder_setup().expect("Error setting up application folder.");
|
||||
|
||||
let auth: Auth = Auth {
|
||||
key: env::var("GODADDY_KEY").expect("You need to set GODADDY_KEY env variable first."),
|
||||
secret: env::var("GODADDY_SECRET")
|
||||
.expect("You need to set GODADDY_SECRET env variable first."),
|
||||
};
|
||||
let domain = env::var("DOMAIN").expect("You need to set DOMAIN env variable first.");
|
||||
let key = env::var("KEY").expect("You need to set KEY env variable first.");
|
||||
let secret = env::var("SECRET").expect("You need to set SECRET env variable first.");
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
records_handler::update(&domain, &key, &secret).await
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
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.
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
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