use std::collections::HashMap; use std::env; use std::env::VarError; use std::error::Error; use std::net::{Ipv4Addr, Ipv6Addr}; use log::{error, warn}; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{Url}; use reqwest::blocking::{Response, Client}; use serde_derive::{Deserialize, Serialize}; use strum_macros::{Display, IntoStaticStr}; use systemd_journal_logger::connected_to_journal; use url::ParseError; use crate::config::{ZoneConfig, ZoneEntry}; const API_BASE: &str = "https://api.cloudflare.com/client/v4"; #[derive(Serialize, Deserialize, Debug)] struct CloudflareApiResults { result: Vec, success: bool, } #[derive(Serialize, Deserialize, Debug)] struct CloudflareApiResult { success: bool, } pub(crate) struct CloudflareZone { name: String, email: String, key: String, id: String, } impl CloudflareZone { pub(crate) fn new(zone: &ZoneConfig) -> Result { let key = env::var("CF_API_TOKEN")?; Ok(Self { name: zone.name.clone(), email: zone.email.clone(), key, id: zone.id.clone(), }) } fn generate_auth_headers(&self) -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert( "X-Auth-Email", HeaderValue::from_str(self.email.as_str()).expect("After CloudflareZone gets created the required Values should exist"), ); headers.insert( "X-Auth-Key", HeaderValue::from_str(self.key.as_str()).expect("After CloudflareZone gets created the required Values should exist"), ); headers } pub(crate) fn get_entries(&self) -> Result, ()> { let endpoint = format!("{}/zones/{}/dns_records", API_BASE, self.id); match self.get(&endpoint) { Ok(response) => { if response.status().is_success() { let entries = match response.json::() { Ok(data) => data, Err(e) => { let err_msg = format!("Unable to parse API response. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } return Err(()) } }; Ok(entries.result) } else { let err_msg = format!("Unable to fetch Cloudflare Zone Entries for {}. Error: {}",self.name ,response.status()); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } Err(e) => { let err_msg = format!("Unable to access Cloudflare API. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) }, } } pub(crate) fn update(&self, entry: &ZoneEntry, r#type: &DnsRecordType, id: &String, ipv6: Option, ipv4: Option) -> Result<(), ()> { let endpoint = format!("{}/zones/{}/dns_records/{}", API_BASE, self.id, id); return match r#type { DnsRecordType::A => { if let Some(ip) = ipv4 { let json_body = self.create_body("A", &entry.name, ip.to_string().as_str()); match self.put(&endpoint, &json_body) { Ok(response) => { self.validate_response(response) }, Err(e) => { let err_msg = format!("Unable to access Cloudflare API. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } } else { let err_msg = "Missing IPv4 for Update."; match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } }, DnsRecordType::AAAA => { if let Some(ip) = ipv6 { let json_body = self.create_body("AAAA", &entry.name, ip.to_string().as_str()); match self.put(&endpoint, &json_body) { Ok(response) => { self.validate_response(response) }, Err(e) => { let err_msg = format!("Unable to access Cloudflare API. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } } else { let err_msg = "Missing IPv6 for Update."; match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } }, _ => { let warn_msg = "Config contains unsupported type identifier"; match connected_to_journal() { true => warn!("[WARN] {warn_msg}"), false => println!("[WARN] {warn_msg}"), } Err(()) } } } pub(crate) fn create(&self, entry: &ZoneEntry, r#type: &DnsRecordType, ipv6: Option, ipv4: Option) -> Result<(), ()> { let endpoint = format!("{}/zones/{}/dns_records", API_BASE, self.id); return match r#type { DnsRecordType::A => { if let Some(ip) = ipv4 { let json_body = self.create_body("A", &entry.name, ip.to_string().as_str()); match self.post(&endpoint, &json_body) { Ok(response) => { self.validate_response(response) }, Err(e) => { let err_msg = format!("Unable to access Cloudflare API. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } } else { let err_msg = "Missing IPv4 for Update."; match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } }, DnsRecordType::AAAA => { if let Some(ip) = ipv6 { let json_body = self.create_body("AAAA", &entry.name, ip.to_string().as_str()); match self.post(&endpoint, &json_body) { Ok(response) => { self.validate_response(response) }, Err(e) => { let err_msg = format!("Unable to access Cloudflare API. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } } else { let err_msg = "Missing IPv6 for Update."; match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } }, _ => { let warn_msg = "Config contains unsupported type identifier"; match connected_to_journal() { true => warn!("[WARN] {warn_msg}"), false => println!("[WARN] {warn_msg}"), } Err(()) } } } fn get(&self, url: &str) -> Result> { let url_parsed = self.parse_url(url)?; match Client::new() .get(url_parsed) .headers(self.generate_auth_headers()) .send() { Ok(result) => Ok(result), Err(e) => Err(Box::new(e)), } } fn post(&self, url: &str, data: &HashMap) -> Result> { let url_parsed = self.parse_url(url)?; match Client::new() .post(url_parsed) .headers(self.generate_auth_headers()) .json(data) .send() { Ok(result) => Ok(result), Err(e) => Err(Box::new(e)), } } fn put(&self, url: &str, data: &HashMap) -> Result> { let url_parsed = self.parse_url(url)?; match Client::new() .put(url_parsed) .headers(self.generate_auth_headers()) .json(data) .send() { Ok(result) => Ok(result), Err(e) => Err(Box::new(e)), } } fn parse_url(&self, input: &str) -> Result { match Url::parse(input) { Ok(url) => Ok(url), Err(e) => { let err_msg = format!("Unable to parse URL. Error: {}", e); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(e) } } } fn create_body(&self, r#type: &str, name: &str, ip: &str) -> HashMap { let mut body = HashMap::new(); body.insert("type".to_owned(), r#type.to_owned()); body.insert("name".to_owned(), name.to_owned()); body.insert("content".to_owned(), ip.to_owned()); body } fn validate_response(&self, response: Response) -> Result<(), ()> { if response.status().is_success() { let data = match response.json::() { Ok(data) => data, Err(e) => { let err_msg = format!("Unable to parse API response. Error: {e}"); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } return Err(()) } }; match data.success { true => Ok(()), false => { let err_msg = format!("Unexpected error while updating DNS record. Info: {:?}", data); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } } else { let err_msg = format!("Unable to post/put Cloudflare DNS entry. Error: {}", response.status()); match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"), } Err(()) } } } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Display, IntoStaticStr)] #[allow(clippy::upper_case_acronyms)] pub(crate) enum DnsRecordType { A, AAAA, CAA, CERT, CNAME, DNSKEY, DS, HTTPS, LOC, MX, NAPTR, NS, PTR, SMIMEA, SRV, SSHFP, SVCB, TLSA, TXT, URI } #[derive(Serialize, Deserialize, PartialEq, Debug)] pub(crate) struct CloudflareDnsRecord { pub(crate) id: String, pub(crate) name: String, pub(crate) r#type: DnsRecordType, pub(crate) content: String }