Compare commits
	
		
			2 Commits
		
	
	
		
			dbdcf5a3ab
			...
			8e43820281
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 8e43820281 | |
|  | 0b128ee2e1 | 
|  | @ -0,0 +1,3 @@ | |||
| GODADDY_DOMAIN="example.com" | ||||
| GODADDY_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" | ||||
| GODADDY_SECRET="XXXXXXXXXXXXXXXXXXXXXX" | ||||
|  | @ -1,10 +1,28 @@ | |||
| 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,10 +14,12 @@ 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 | ||||
| lto = true | ||||
| debug = false | ||||
| codegen-units = 1 | ||||
| panic = "abort" | ||||
| panic = "abort" | ||||
|  |  | |||
|  | @ -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::error::Error; | ||||
| 
 | ||||
| use log::LevelFilter; | ||||
| use simple_logger::SimpleLogger; | ||||
| 
 | ||||
| mod file_handler; | ||||
| mod records_handler; | ||||
| mod ip_handler; | ||||
| 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, | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn Error>> { | ||||
|  | @ -17,11 +85,43 @@ async fn main() -> Result<(), Box<dyn Error>> { | |||
|         .init() | ||||
|         .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 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 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."), | ||||
|     }; | ||||
| 
 | ||||
|     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