From 9c141f884de571d2cd08d151f6fe2b09daf361f7 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 14:17:56 +0300 Subject: [PATCH 01/12] cargo fmt --- src/main.rs | 504 +++++++++++++++++++++++++++++----------------------- 1 file changed, 278 insertions(+), 226 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3af84a6..8a421c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,244 +11,296 @@ const DEFAULT_IPS: [&str; 2] = ["https://ipv4.icanhazip.com", "https://api.ipify #[derive(Debug, Serialize, Deserialize)] struct CloudflareDDNS { - ip_src: Option>, - auth_key: String, - auth_email: String, - zone_id: String, - patterns: Option>, - invert_patterns: Option, - http_timeout_s: Option, + ip_src: Option>, + auth_key: String, + auth_email: String, + zone_id: String, + patterns: Option>, + invert_patterns: Option, + http_timeout_s: Option, } impl Default for CloudflareDDNS { - fn default() -> Self { - Self { - ip_src: Some(DEFAULT_IPS.into_iter().map(String::from).collect()), - auth_key: Default::default(), - auth_email: Default::default(), - zone_id: Default::default(), - patterns: None, - invert_patterns: None, - http_timeout_s: Some(10), - } - } + fn default() -> Self { + Self { + ip_src: Some(DEFAULT_IPS.into_iter().map(String::from).collect()), + auth_key: Default::default(), + auth_email: Default::default(), + zone_id: Default::default(), + patterns: None, + invert_patterns: None, + http_timeout_s: Some(10), + } + } } #[derive(Debug, Default, Deserialize)] struct CloudflareDnsResponse { - success: bool, - #[serde(rename = "result")] - entries: Vec, - errors: Vec, + success: bool, + #[serde(rename = "result")] + entries: Vec, + errors: Vec, } #[derive(Debug, Deserialize)] struct CloudflareDnsRecord { - id: String, - r#type: String, - name: String, - #[serde(rename = "content")] - ip: Ipv4Addr, + id: String, + r#type: String, + name: String, + #[serde(rename = "content")] + ip: Ipv4Addr, } fn main() { - let conf: CloudflareDDNS; - let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); - let conf_path = conf_dir.join("config.toml"); - - if !conf_path.exists() { - if let Err(e) = std::fs::create_dir_all(&conf_dir) { - println!("couldn't create config dir: {e}"); - } else { - match std::fs::File::create(&conf_path) { - Ok(mut f) => { - conf = CloudflareDDNS::default(); - let data = toml::to_string_pretty(&conf).unwrap(); - if let Err(e) = f.write_all(data.as_bytes()) { - println!("couldn't write default config file: {e}"); - } else { - println!("default config file created at {conf_path:?}\nrequired fields: auth_key, auth_email, zone_id"); - } - }, - Err(e) => { - println!("couldn't create config.toml: {e}"); - }, - } - } - exit(1); - } - - match Config::builder() - .set_default("ip_src", DEFAULT_IPS.into_iter().map(String::from).collect::>()).unwrap() - .set_default("http_timeout_s", 10).unwrap() - .add_source(config::File::with_name(conf_path.to_str().unwrap())) - .add_source(config::Environment::with_prefix("CF")) - .build() - { - Ok(c) => { - match c.try_deserialize::() { - Ok(c) => conf = c, - Err(e) => { - println!("config error: {e:?}"); - exit(1); - }, - }; - }, - Err(e) => { - println!("couldn't parse config: {e}"); - exit(1); - }, - } - - /* detect missing config entries */ { - let mut missing = Vec::<&str>::new(); - if conf.auth_key.is_empty() { missing.push("auth_key"); } - if conf.auth_email.is_empty() { missing.push("auth_email"); } - if conf.zone_id.is_empty() { missing.push("zone_id"); } - if !missing.is_empty() { - println!("missing configuration entries: {}", missing.join(", ")); - exit(1); - } - } - - let client: ureq::Agent = ureq::Agent::config_builder() - .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) - .timeout_global(Some(std::time::Duration::from_secs(conf.http_timeout_s.unwrap()))) - .https_only(true) - .ip_family(Ipv4Only) - .build() - .into(); - - println!("> getting external ipv4 address..."); - - let ipv4 = conf.ip_src.as_ref().unwrap().iter().find_map(|ip| { - print!("trying {ip}: "); - - match client.get(ip).call() { - Ok(resp) => { - match resp.into_body().read_to_string() { - Ok(body) => { - match Ipv4Addr::from_str(body.trim()) { - Ok(ip_addr) => Some(ip_addr), - Err(e) => { - println!("failed: {e}"); - None - }, - } - }, - Err(e) => { - println!("failed: {e}"); - None - }, - } - }, - Err(e) => { - println!("failed: {e}"); - None - }, - } - }).unwrap_or_else(|| { - println!("could not determine external ip address"); - exit(1); - }); - - print!("{ipv4:?}\n> listing dns A-records... "); - - match client.get(format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", conf.zone_id)) - .header("X-Auth-Email", &conf.auth_email) - .header("Authorization", format!("Bearer {}", conf.auth_key)) - .call() { - Ok(resp) => { - match resp.into_body().read_json::>() { - Ok(resp) => { - if !resp.success { - println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); - exit(1); - } - - let mut a_records = resp.entries.iter() - .filter(|x| x.r#type == "A") - .collect::>(); - - let total_records = a_records.len(); - if total_records == 0 { - println!("none found"); - exit(0); - } - - if let Some(patterns) = conf.patterns.as_ref() { - let matchers = patterns.iter() - .map(|p| globset::Glob::new(p).expect("invalid pattern").compile_matcher()) - .collect::>(); - - a_records.retain(|x| { - let matched = matchers.iter().any(|m| m.is_match(&x.name)); - if conf.invert_patterns.unwrap_or(true) { !matched } else { matched } - }); - } - - let filtered_records = a_records.len(); - if filtered_records == 0 { - println!("all records were filtered"); - exit(0); - } - - print!("{} found", total_records); - if total_records > filtered_records { - print!(", {} filtered", total_records - filtered_records); - } - - println!("\n> patching...", ); - - let mut errors = false; - - for (i, record) in a_records.into_iter().enumerate() { - if record.ip == ipv4 { - println!("record {} ({}): up to date", i + 1, record.name); - continue; - } - print!("record {}: ", i + 1); - - match client.patch(format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", conf.zone_id, record.id)) - .header("X-Auth-Email", &conf.auth_email) - .header("Authorization", format!("Bearer {}", conf.auth_key)) - .send_json(json!({"content": ipv4.to_string()})) { - Ok(resp) => { - if resp.status().is_success() { - println!("success"); - } else { - let status = resp.status(); - println!("failed (http {})", status); - - let data = resp.into_body().read_json::>().unwrap_or_default(); - if !data.errors.is_empty() { - println!("error(s):\n{}", data.errors.join("\n")); - } - } - }, - Err(e) => { - println!("failed: {e}"); - errors = true; - } - } - } - - if errors { - println!("finished with errors"); - exit(1); - } - }, - Err(e) => { - println!("failed:\n{e}"); - exit(1); - }, - } - }, - Err(e) => { - println!("failed:\n{e}"); - exit(1); - }, - } - - println!("finished"); + let conf: CloudflareDDNS; + let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); + let conf_path = conf_dir.join("config.toml"); + + if !conf_path.exists() { + if let Err(e) = std::fs::create_dir_all(&conf_dir) { + println!("couldn't create config dir: {e}"); + } else { + match std::fs::File::create(&conf_path) { + Ok(mut f) => { + conf = CloudflareDDNS::default(); + let data = toml::to_string_pretty(&conf).unwrap(); + if let Err(e) = f.write_all(data.as_bytes()) { + println!("couldn't write default config file: {e}"); + } else { + println!( + "default config file created at {conf_path:?}\nrequired fields: auth_key, auth_email, zone_id" + ); + } + } + Err(e) => { + println!("couldn't create config.toml: {e}"); + } + } + } + exit(1); + } + + match Config::builder() + .set_default( + "ip_src", + DEFAULT_IPS + .into_iter() + .map(String::from) + .collect::>(), + ) + .unwrap() + .set_default("http_timeout_s", 10) + .unwrap() + .add_source(config::File::with_name(conf_path.to_str().unwrap())) + .add_source(config::Environment::with_prefix("CF")) + .build() + { + Ok(c) => { + match c.try_deserialize::() { + Ok(c) => conf = c, + Err(e) => { + println!("config error: {e:?}"); + exit(1); + } + }; + } + Err(e) => { + println!("couldn't parse config: {e}"); + exit(1); + } + } + + /* detect missing config entries */ + { + let mut missing = Vec::<&str>::new(); + if conf.auth_key.is_empty() { + missing.push("auth_key"); + } + if conf.auth_email.is_empty() { + missing.push("auth_email"); + } + if conf.zone_id.is_empty() { + missing.push("zone_id"); + } + if !missing.is_empty() { + println!("missing configuration entries: {}", missing.join(", ")); + exit(1); + } + } + + let client: ureq::Agent = ureq::Agent::config_builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )) + .timeout_global(Some(std::time::Duration::from_secs( + conf.http_timeout_s.unwrap(), + ))) + .https_only(true) + .ip_family(Ipv4Only) + .build() + .into(); + + println!("> getting external ipv4 address..."); + + let ipv4 = conf + .ip_src + .as_ref() + .unwrap() + .iter() + .find_map(|ip| { + print!("trying {ip}: "); + + match client.get(ip).call() { + Ok(resp) => match resp.into_body().read_to_string() { + Ok(body) => match Ipv4Addr::from_str(body.trim()) { + Ok(ip_addr) => Some(ip_addr), + Err(e) => { + println!("failed: {e}"); + None + } + }, + Err(e) => { + println!("failed: {e}"); + None + } + }, + Err(e) => { + println!("failed: {e}"); + None + } + } + }) + .unwrap_or_else(|| { + println!("could not determine external ip address"); + exit(1); + }); + + print!("{ipv4:?}\n> listing dns A-records... "); + + match client + .get(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", + conf.zone_id + )) + .header("X-Auth-Email", &conf.auth_email) + .header("Authorization", format!("Bearer {}", conf.auth_key)) + .call() + { + Ok(resp) => { + match resp + .into_body() + .read_json::>() + { + Ok(resp) => { + if !resp.success { + println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); + exit(1); + } + + let mut a_records = resp + .entries + .iter() + .filter(|x| x.r#type == "A") + .collect::>(); + + let total_records = a_records.len(); + if total_records == 0 { + println!("none found"); + exit(0); + } + + if let Some(patterns) = conf.patterns.as_ref() { + let matchers = patterns + .iter() + .map(|p| { + globset::Glob::new(p) + .expect("invalid pattern") + .compile_matcher() + }) + .collect::>(); + + a_records.retain(|x| { + let matched = matchers.iter().any(|m| m.is_match(&x.name)); + if conf.invert_patterns.unwrap_or(true) { + !matched + } else { + matched + } + }); + } + + let filtered_records = a_records.len(); + if filtered_records == 0 { + println!("all records were filtered"); + exit(0); + } + + print!("{} found", total_records); + if total_records > filtered_records { + print!(", {} filtered", total_records - filtered_records); + } + + println!("\n> patching...",); + + let mut errors = false; + + for (i, record) in a_records.into_iter().enumerate() { + if record.ip == ipv4 { + println!("record {} ({}): up to date", i + 1, record.name); + continue; + } + print!("record {}: ", i + 1); + + match client + .patch(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", + conf.zone_id, record.id + )) + .header("X-Auth-Email", &conf.auth_email) + .header("Authorization", format!("Bearer {}", conf.auth_key)) + .send_json(json!({"content": ipv4.to_string()})) + { + Ok(resp) => { + if resp.status().is_success() { + println!("success"); + } else { + let status = resp.status(); + println!("failed (http {})", status); + + let data = resp + .into_body() + .read_json::>() + .unwrap_or_default(); + if !data.errors.is_empty() { + println!("error(s):\n{}", data.errors.join("\n")); + } + } + } + Err(e) => { + println!("failed: {e}"); + errors = true; + } + } + } + + if errors { + println!("finished with errors"); + exit(1); + } + } + Err(e) => { + println!("failed:\n{e}"); + exit(1); + } + } + } + Err(e) => { + println!("failed:\n{e}"); + exit(1); + } + } + + println!("finished"); } From d0112c1aed82acd580302e2cbdba85fdc3e564b5 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 15:14:05 +0300 Subject: [PATCH 02/12] split into some functions --- src/main.rs | 418 +++++++++++++++++++++++++++------------------------- 1 file changed, 216 insertions(+), 202 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8a421c8..43c6bad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,110 +52,128 @@ struct CloudflareDnsRecord { } fn main() { - let conf: CloudflareDDNS; - let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); - let conf_path = conf_dir.join("config.toml"); + let cddns = CloudflareDDNS::get_configured(); + cddns.run(); +} + +impl CloudflareDDNS { + fn get_configured() -> Self { + let conf: CloudflareDDNS; + let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); + let conf_path = conf_dir.join("config.toml"); - if !conf_path.exists() { - if let Err(e) = std::fs::create_dir_all(&conf_dir) { - println!("couldn't create config dir: {e}"); - } else { - match std::fs::File::create(&conf_path) { - Ok(mut f) => { - conf = CloudflareDDNS::default(); - let data = toml::to_string_pretty(&conf).unwrap(); - if let Err(e) = f.write_all(data.as_bytes()) { - println!("couldn't write default config file: {e}"); - } else { - println!( - "default config file created at {conf_path:?}\nrequired fields: auth_key, auth_email, zone_id" - ); + if !conf_path.exists() { + if let Err(e) = std::fs::create_dir_all(&conf_dir) { + println!("couldn't create config dir: {e}"); + } else { + match std::fs::File::create(&conf_path) { + Ok(mut f) => { + conf = CloudflareDDNS::default(); + let data = toml::to_string_pretty(&conf).unwrap(); + if let Err(e) = f.write_all(data.as_bytes()) { + println!("couldn't write default config file: {e}"); + } else { + println!( + "default config file created at {conf_path:?}\nrequired fields: auth_key, auth_email, zone_id" + ); + } + } + Err(e) => { + println!("couldn't create config.toml: {e}"); } - } - Err(e) => { - println!("couldn't create config.toml: {e}"); } } + exit(1); } - exit(1); - } - match Config::builder() - .set_default( - "ip_src", - DEFAULT_IPS - .into_iter() - .map(String::from) - .collect::>(), - ) - .unwrap() - .set_default("http_timeout_s", 10) - .unwrap() - .add_source(config::File::with_name(conf_path.to_str().unwrap())) - .add_source(config::Environment::with_prefix("CF")) - .build() - { - Ok(c) => { - match c.try_deserialize::() { - Ok(c) => conf = c, - Err(e) => { - println!("config error: {e:?}"); - exit(1); - } - }; + match Config::builder() + .set_default( + "ip_src", + DEFAULT_IPS + .into_iter() + .map(String::from) + .collect::>(), + ) + .unwrap() + .set_default("http_timeout_s", 10) + .unwrap() + .add_source(config::File::with_name(conf_path.to_str().unwrap())) + .add_source(config::Environment::with_prefix("CF")) + .build() + { + Ok(c) => { + match c.try_deserialize::() { + Ok(c) => conf = c, + Err(e) => { + println!("config error: {e:?}"); + exit(1); + } + }; + } + Err(e) => { + println!("couldn't parse config: {e}"); + exit(1); + } } - Err(e) => { - println!("couldn't parse config: {e}"); - exit(1); + + /* detect missing config entries */ + { + let mut missing = Vec::<&str>::new(); + if conf.auth_key.is_empty() { + missing.push("auth_key"); + } + if conf.auth_email.is_empty() { + missing.push("auth_email"); + } + if conf.zone_id.is_empty() { + missing.push("zone_id"); + } + if !missing.is_empty() { + println!("missing configuration entries: {}", missing.join(", ")); + exit(1); + } } + conf } - /* detect missing config entries */ - { - let mut missing = Vec::<&str>::new(); - if conf.auth_key.is_empty() { - missing.push("auth_key"); - } - if conf.auth_email.is_empty() { - missing.push("auth_email"); - } - if conf.zone_id.is_empty() { - missing.push("zone_id"); - } - if !missing.is_empty() { - println!("missing configuration entries: {}", missing.join(", ")); - exit(1); - } + fn get_client(&self) -> ureq::Agent { + ureq::Agent::config_builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )) + .timeout_global(Some(std::time::Duration::from_secs( + self.http_timeout_s.unwrap(), + ))) + .https_only(true) + .ip_family(Ipv4Only) + .build() + .into() } - let client: ureq::Agent = ureq::Agent::config_builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )) - .timeout_global(Some(std::time::Duration::from_secs( - conf.http_timeout_s.unwrap(), - ))) - .https_only(true) - .ip_family(Ipv4Only) - .build() - .into(); + fn run(self) { + let client = self.get_client(); - println!("> getting external ipv4 address..."); + println!("> getting external ipv4 address..."); - let ipv4 = conf - .ip_src - .as_ref() - .unwrap() - .iter() - .find_map(|ip| { - print!("trying {ip}: "); + let ipv4 = self + .ip_src + .as_ref() + .unwrap() + .iter() + .find_map(|ip| { + print!("trying {ip}: "); - match client.get(ip).call() { - Ok(resp) => match resp.into_body().read_to_string() { - Ok(body) => match Ipv4Addr::from_str(body.trim()) { - Ok(ip_addr) => Some(ip_addr), + match client.get(ip).call() { + Ok(resp) => match resp.into_body().read_to_string() { + Ok(body) => match Ipv4Addr::from_str(body.trim()) { + Ok(ip_addr) => Some(ip_addr), + Err(e) => { + println!("failed: {e}"); + None + } + }, Err(e) => { println!("failed: {e}"); None @@ -165,142 +183,138 @@ fn main() { println!("failed: {e}"); None } - }, - Err(e) => { - println!("failed: {e}"); - None } - } - }) - .unwrap_or_else(|| { - println!("could not determine external ip address"); - exit(1); - }); - - print!("{ipv4:?}\n> listing dns A-records... "); + }) + .unwrap_or_else(|| { + println!("could not determine external ip address"); + exit(1); + }); - match client - .get(format!( - "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", - conf.zone_id - )) - .header("X-Auth-Email", &conf.auth_email) - .header("Authorization", format!("Bearer {}", conf.auth_key)) - .call() - { - Ok(resp) => { - match resp - .into_body() - .read_json::>() - { - Ok(resp) => { - if !resp.success { - println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); - exit(1); - } + print!("{ipv4:?}\n> listing dns A-records... "); - let mut a_records = resp - .entries - .iter() - .filter(|x| x.r#type == "A") - .collect::>(); - - let total_records = a_records.len(); - if total_records == 0 { - println!("none found"); - exit(0); - } + match client + .get(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", + self.zone_id + )) + .header("X-Auth-Email", &self.auth_email) + .header("Authorization", format!("Bearer {}", self.auth_key)) + .call() + { + Ok(resp) => { + match resp + .into_body() + .read_json::>() + { + Ok(resp) => { + if !resp.success { + println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); + exit(1); + } - if let Some(patterns) = conf.patterns.as_ref() { - let matchers = patterns + let mut a_records = resp + .entries .iter() - .map(|p| { - globset::Glob::new(p) - .expect("invalid pattern") - .compile_matcher() - }) + .filter(|x| x.r#type == "A") .collect::>(); - a_records.retain(|x| { - let matched = matchers.iter().any(|m| m.is_match(&x.name)); - if conf.invert_patterns.unwrap_or(true) { - !matched - } else { - matched - } - }); - } - - let filtered_records = a_records.len(); - if filtered_records == 0 { - println!("all records were filtered"); - exit(0); - } + let total_records = a_records.len(); + if total_records == 0 { + println!("none found"); + exit(0); + } - print!("{} found", total_records); - if total_records > filtered_records { - print!(", {} filtered", total_records - filtered_records); - } + if let Some(patterns) = self.patterns.as_ref() { + let matchers = patterns + .iter() + .map(|p| { + globset::Glob::new(p) + .expect("invalid pattern") + .compile_matcher() + }) + .collect::>(); - println!("\n> patching...",); + a_records.retain(|x| { + let matched = matchers.iter().any(|m| m.is_match(&x.name)); + if self.invert_patterns.unwrap_or(true) { + !matched + } else { + matched + } + }); + } - let mut errors = false; + let filtered_records = a_records.len(); + if filtered_records == 0 { + println!("all records were filtered"); + exit(0); + } - for (i, record) in a_records.into_iter().enumerate() { - if record.ip == ipv4 { - println!("record {} ({}): up to date", i + 1, record.name); - continue; + print!("{} found", total_records); + if total_records > filtered_records { + print!(", {} filtered", total_records - filtered_records); } - print!("record {}: ", i + 1); - match client - .patch(format!( - "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", - conf.zone_id, record.id - )) - .header("X-Auth-Email", &conf.auth_email) - .header("Authorization", format!("Bearer {}", conf.auth_key)) - .send_json(json!({"content": ipv4.to_string()})) - { - Ok(resp) => { - if resp.status().is_success() { - println!("success"); - } else { - let status = resp.status(); - println!("failed (http {})", status); + println!("\n> patching...",); + + let mut errors = false; + + for (i, record) in a_records.into_iter().enumerate() { + if record.ip == ipv4 { + println!("record {} ({}): up to date", i + 1, record.name); + continue; + } + print!("record {}: ", i + 1); + + match client + .patch(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", + self.zone_id, record.id + )) + .header("X-Auth-Email", &self.auth_email) + .header("Authorization", format!("Bearer {}", self.auth_key)) + .send_json(json!({"content": ipv4.to_string()})) + { + Ok(resp) => { + if resp.status().is_success() { + println!("success"); + } else { + let status = resp.status(); + println!("failed (http {})", status); - let data = resp - .into_body() - .read_json::>() - .unwrap_or_default(); - if !data.errors.is_empty() { - println!("error(s):\n{}", data.errors.join("\n")); + let data = resp + .into_body() + .read_json::>() + .unwrap_or_default(); + if !data.errors.is_empty() { + println!("error(s):\n{}", data.errors.join("\n")); + } } } - } - Err(e) => { - println!("failed: {e}"); - errors = true; + Err(e) => { + println!("failed: {e}"); + errors = true; + } } } - } - if errors { - println!("finished with errors"); + if errors { + println!("finished with errors"); + exit(1); + } + } + Err(e) => { + println!("failed:\n{e}"); exit(1); } } - Err(e) => { - println!("failed:\n{e}"); - exit(1); - } + } + Err(e) => { + println!("failed:\n{e}"); + exit(1); } } - Err(e) => { - println!("failed:\n{e}"); - exit(1); - } - } - println!("finished"); + println!("finished"); + } } From c9d622edc82cf8b3717121721ceb08607257b239 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 15:35:31 +0300 Subject: [PATCH 03/12] more functions --- src/main.rs | 60 +++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index 43c6bad..ed1d2f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::io::Write; use std::net::Ipv4Addr; use std::process::exit; use std::str::FromStr; +use ureq::Agent; use ureq::config::IpFamily::Ipv4Only; const DEFAULT_IPS: [&str; 2] = ["https://ipv4.icanhazip.com", "https://api.ipify.org"]; @@ -116,28 +117,30 @@ impl CloudflareDDNS { } } - /* detect missing config entries */ - { - let mut missing = Vec::<&str>::new(); - if conf.auth_key.is_empty() { - missing.push("auth_key"); - } - if conf.auth_email.is_empty() { - missing.push("auth_email"); - } - if conf.zone_id.is_empty() { - missing.push("zone_id"); - } - if !missing.is_empty() { - println!("missing configuration entries: {}", missing.join(", ")); - exit(1); - } - } + conf.check_missing(); + conf } - fn get_client(&self) -> ureq::Agent { - ureq::Agent::config_builder() + fn check_missing(&self) { + let mut missing = Vec::<&str>::new(); + if self.auth_key.is_empty() { + missing.push("auth_key"); + } + if self.auth_email.is_empty() { + missing.push("auth_email"); + } + if self.zone_id.is_empty() { + missing.push("zone_id"); + } + if !missing.is_empty() { + println!("missing configuration entries: {}", missing.join(", ")); + exit(1); + } + } + + fn get_client(&self) -> Agent { + Agent::config_builder() .user_agent(concat!( env!("CARGO_PKG_NAME"), "/", @@ -152,13 +155,8 @@ impl CloudflareDDNS { .into() } - fn run(self) { - let client = self.get_client(); - - println!("> getting external ipv4 address..."); - - let ipv4 = self - .ip_src + fn get_ipv4(&self, client: &Agent) -> Ipv4Addr { + self.ip_src .as_ref() .unwrap() .iter() @@ -188,7 +186,15 @@ impl CloudflareDDNS { .unwrap_or_else(|| { println!("could not determine external ip address"); exit(1); - }); + }) + } + + fn run(self) { + let client = self.get_client(); + + println!("> getting external ipv4 address..."); + + let ipv4 = self.get_ipv4(&client); print!("{ipv4:?}\n> listing dns A-records... "); From c1b79dd78d0efcfaffb77c24b4126a813c8b7dde Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 16:57:41 +0300 Subject: [PATCH 04/12] more functions 2 --- src/main.rs | 123 +++++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index ed1d2f2..74555e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ impl Default for CloudflareDDNS { } } -#[derive(Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize)] struct CloudflareDnsResponse { success: bool, #[serde(rename = "result")] @@ -43,7 +43,7 @@ struct CloudflareDnsResponse { errors: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] struct CloudflareDnsRecord { id: String, r#type: String, @@ -155,7 +155,7 @@ impl CloudflareDDNS { .into() } - fn get_ipv4(&self, client: &Agent) -> Ipv4Addr { + fn get_current_ipv4(&self, client: &Agent) -> Ipv4Addr { self.ip_src .as_ref() .unwrap() @@ -194,9 +194,17 @@ impl CloudflareDDNS { println!("> getting external ipv4 address..."); - let ipv4 = self.get_ipv4(&client); + let current_ip = self.get_current_ipv4(&client); - print!("{ipv4:?}\n> listing dns A-records... "); + let a_records = self.get_a_records(&client, ¤t_ip); + + self.patch_records(¤t_ip, &client, a_records); + + println!("finished"); + } + + fn get_a_records(&self, client: &Agent, current_ip: &Ipv4Addr) -> Vec { + print!("{current_ip:?}\n> listing dns A-records... "); match client .get(format!( @@ -217,11 +225,11 @@ impl CloudflareDDNS { println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); exit(1); } - let mut a_records = resp .entries .iter() .filter(|x| x.r#type == "A") + .map(|x| x.to_owned()) .collect::>(); let total_records = a_records.len(); @@ -261,53 +269,7 @@ impl CloudflareDDNS { print!(", {} filtered", total_records - filtered_records); } - println!("\n> patching...",); - - let mut errors = false; - - for (i, record) in a_records.into_iter().enumerate() { - if record.ip == ipv4 { - println!("record {} ({}): up to date", i + 1, record.name); - continue; - } - print!("record {}: ", i + 1); - - match client - .patch(format!( - "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", - self.zone_id, record.id - )) - .header("X-Auth-Email", &self.auth_email) - .header("Authorization", format!("Bearer {}", self.auth_key)) - .send_json(json!({"content": ipv4.to_string()})) - { - Ok(resp) => { - if resp.status().is_success() { - println!("success"); - } else { - let status = resp.status(); - println!("failed (http {})", status); - - let data = resp - .into_body() - .read_json::>() - .unwrap_or_default(); - if !data.errors.is_empty() { - println!("error(s):\n{}", data.errors.join("\n")); - } - } - } - Err(e) => { - println!("failed: {e}"); - errors = true; - } - } - } - - if errors { - println!("finished with errors"); - exit(1); - } + return a_records; } Err(e) => { println!("failed:\n{e}"); @@ -320,7 +282,60 @@ impl CloudflareDDNS { exit(1); } } + } - println!("finished"); + fn patch_records( + &self, + current_ip: &Ipv4Addr, + client: &Agent, + a_records: Vec, + ) { + println!("\n> patching...",); + + let mut errors = false; + + for (i, record) in a_records.into_iter().enumerate() { + if record.ip == *current_ip { + println!("record {} ({}): up to date", i + 1, record.name); + continue; + } + print!("record {}: ", i + 1); + + match client + .patch(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", + self.zone_id, record.id + )) + .header("X-Auth-Email", &self.auth_email) + .header("Authorization", format!("Bearer {}", self.auth_key)) + .send_json(json!({"content": current_ip.to_string()})) + { + Ok(resp) => { + if resp.status().is_success() { + println!("success"); + } else { + let status = resp.status(); + println!("failed (http {})", status); + + let data = resp + .into_body() + .read_json::>() + .unwrap_or_default(); + if !data.errors.is_empty() { + println!("error(s):\n{}", data.errors.join("\n")); + } + } + } + Err(e) => { + println!("failed: {e}"); + errors = true; + } + } + } + + if errors { + println!("finished with errors"); + exit(1); + } } } From 599cc8f265e6dc06a70d3e7597e3a0a367d2d006 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 17:03:27 +0300 Subject: [PATCH 05/12] minor cleanup --- src/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 74555e0..778b385 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,12 +53,12 @@ struct CloudflareDnsRecord { } fn main() { - let cddns = CloudflareDDNS::get_configured(); + let cddns = CloudflareDDNS::get_from_config(); cddns.run(); } impl CloudflareDDNS { - fn get_configured() -> Self { + fn get_from_config() -> Self { let conf: CloudflareDDNS; let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); let conf_path = conf_dir.join("config.toml"); @@ -156,6 +156,7 @@ impl CloudflareDDNS { } fn get_current_ipv4(&self, client: &Agent) -> Ipv4Addr { + println!("> getting external ipv4 address..."); self.ip_src .as_ref() .unwrap() @@ -192,13 +193,11 @@ impl CloudflareDDNS { fn run(self) { let client = self.get_client(); - println!("> getting external ipv4 address..."); - let current_ip = self.get_current_ipv4(&client); let a_records = self.get_a_records(&client, ¤t_ip); - self.patch_records(¤t_ip, &client, a_records); + self.patch_records(&client, ¤t_ip, a_records); println!("finished"); } @@ -286,8 +285,8 @@ impl CloudflareDDNS { fn patch_records( &self, - current_ip: &Ipv4Addr, client: &Agent, + current_ip: &Ipv4Addr, a_records: Vec, ) { println!("\n> patching...",); From 70b0ece784ab13e9734093dee4fe181802e49753 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 17:14:00 +0300 Subject: [PATCH 06/12] fix indent to tabs --- src/main.rs | 614 ++++++++++++++++++++++++++-------------------------- 1 file changed, 307 insertions(+), 307 deletions(-) diff --git a/src/main.rs b/src/main.rs index 778b385..db68f92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,329 +12,329 @@ const DEFAULT_IPS: [&str; 2] = ["https://ipv4.icanhazip.com", "https://api.ipify #[derive(Debug, Serialize, Deserialize)] struct CloudflareDDNS { - ip_src: Option>, - auth_key: String, - auth_email: String, - zone_id: String, - patterns: Option>, - invert_patterns: Option, - http_timeout_s: Option, + ip_src: Option>, + auth_key: String, + auth_email: String, + zone_id: String, + patterns: Option>, + invert_patterns: Option, + http_timeout_s: Option, } impl Default for CloudflareDDNS { - fn default() -> Self { - Self { - ip_src: Some(DEFAULT_IPS.into_iter().map(String::from).collect()), - auth_key: Default::default(), - auth_email: Default::default(), - zone_id: Default::default(), - patterns: None, - invert_patterns: None, - http_timeout_s: Some(10), - } - } + fn default() -> Self { + Self { + ip_src: Some(DEFAULT_IPS.into_iter().map(String::from).collect()), + auth_key: Default::default(), + auth_email: Default::default(), + zone_id: Default::default(), + patterns: None, + invert_patterns: None, + http_timeout_s: Some(10), + } + } } #[derive(Clone, Debug, Default, Deserialize)] struct CloudflareDnsResponse { - success: bool, - #[serde(rename = "result")] - entries: Vec, - errors: Vec, + success: bool, + #[serde(rename = "result")] + entries: Vec, + errors: Vec, } #[derive(Clone, Debug, Deserialize)] struct CloudflareDnsRecord { - id: String, - r#type: String, - name: String, - #[serde(rename = "content")] - ip: Ipv4Addr, + id: String, + r#type: String, + name: String, + #[serde(rename = "content")] + ip: Ipv4Addr, } fn main() { - let cddns = CloudflareDDNS::get_from_config(); - cddns.run(); + let cddns = CloudflareDDNS::get_from_config(); + cddns.run(); } impl CloudflareDDNS { - fn get_from_config() -> Self { - let conf: CloudflareDDNS; - let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); - let conf_path = conf_dir.join("config.toml"); - - if !conf_path.exists() { - if let Err(e) = std::fs::create_dir_all(&conf_dir) { - println!("couldn't create config dir: {e}"); - } else { - match std::fs::File::create(&conf_path) { - Ok(mut f) => { - conf = CloudflareDDNS::default(); - let data = toml::to_string_pretty(&conf).unwrap(); - if let Err(e) = f.write_all(data.as_bytes()) { - println!("couldn't write default config file: {e}"); - } else { - println!( - "default config file created at {conf_path:?}\nrequired fields: auth_key, auth_email, zone_id" - ); - } - } - Err(e) => { - println!("couldn't create config.toml: {e}"); - } - } - } - exit(1); - } - - match Config::builder() - .set_default( - "ip_src", - DEFAULT_IPS - .into_iter() - .map(String::from) - .collect::>(), - ) - .unwrap() - .set_default("http_timeout_s", 10) - .unwrap() - .add_source(config::File::with_name(conf_path.to_str().unwrap())) - .add_source(config::Environment::with_prefix("CF")) - .build() - { - Ok(c) => { - match c.try_deserialize::() { - Ok(c) => conf = c, - Err(e) => { - println!("config error: {e:?}"); - exit(1); - } - }; - } - Err(e) => { - println!("couldn't parse config: {e}"); - exit(1); - } - } - - conf.check_missing(); - - conf - } - - fn check_missing(&self) { - let mut missing = Vec::<&str>::new(); - if self.auth_key.is_empty() { - missing.push("auth_key"); - } - if self.auth_email.is_empty() { - missing.push("auth_email"); - } - if self.zone_id.is_empty() { - missing.push("zone_id"); - } - if !missing.is_empty() { - println!("missing configuration entries: {}", missing.join(", ")); - exit(1); - } - } - - fn get_client(&self) -> Agent { - Agent::config_builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )) - .timeout_global(Some(std::time::Duration::from_secs( - self.http_timeout_s.unwrap(), - ))) - .https_only(true) - .ip_family(Ipv4Only) - .build() - .into() - } - - fn get_current_ipv4(&self, client: &Agent) -> Ipv4Addr { + fn get_from_config() -> Self { + let conf: CloudflareDDNS; + let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); + let conf_path = conf_dir.join("config.toml"); + + if !conf_path.exists() { + if let Err(e) = std::fs::create_dir_all(&conf_dir) { + println!("couldn't create config dir: {e}"); + } else { + match std::fs::File::create(&conf_path) { + Ok(mut f) => { + conf = CloudflareDDNS::default(); + let data = toml::to_string_pretty(&conf).unwrap(); + if let Err(e) = f.write_all(data.as_bytes()) { + println!("couldn't write default config file: {e}"); + } else { + println!( + "default config file created at {conf_path:?}\nrequired fields: auth_key, auth_email, zone_id" + ); + } + } + Err(e) => { + println!("couldn't create config.toml: {e}"); + } + } + } + exit(1); + } + + match Config::builder() + .set_default( + "ip_src", + DEFAULT_IPS + .into_iter() + .map(String::from) + .collect::>(), + ) + .unwrap() + .set_default("http_timeout_s", 10) + .unwrap() + .add_source(config::File::with_name(conf_path.to_str().unwrap())) + .add_source(config::Environment::with_prefix("CF")) + .build() + { + Ok(c) => { + match c.try_deserialize::() { + Ok(c) => conf = c, + Err(e) => { + println!("config error: {e:?}"); + exit(1); + } + }; + } + Err(e) => { + println!("couldn't parse config: {e}"); + exit(1); + } + } + + conf.check_missing(); + + conf + } + + fn check_missing(&self) { + let mut missing = Vec::<&str>::new(); + if self.auth_key.is_empty() { + missing.push("auth_key"); + } + if self.auth_email.is_empty() { + missing.push("auth_email"); + } + if self.zone_id.is_empty() { + missing.push("zone_id"); + } + if !missing.is_empty() { + println!("missing configuration entries: {}", missing.join(", ")); + exit(1); + } + } + + fn get_client(&self) -> Agent { + Agent::config_builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )) + .timeout_global(Some(std::time::Duration::from_secs( + self.http_timeout_s.unwrap(), + ))) + .https_only(true) + .ip_family(Ipv4Only) + .build() + .into() + } + + fn get_current_ipv4(&self, client: &Agent) -> Ipv4Addr { println!("> getting external ipv4 address..."); - self.ip_src - .as_ref() - .unwrap() - .iter() - .find_map(|ip| { - print!("trying {ip}: "); - - match client.get(ip).call() { - Ok(resp) => match resp.into_body().read_to_string() { - Ok(body) => match Ipv4Addr::from_str(body.trim()) { - Ok(ip_addr) => Some(ip_addr), - Err(e) => { - println!("failed: {e}"); - None - } - }, - Err(e) => { - println!("failed: {e}"); - None - } - }, - Err(e) => { - println!("failed: {e}"); - None - } - } - }) - .unwrap_or_else(|| { - println!("could not determine external ip address"); - exit(1); - }) - } - - fn run(self) { - let client = self.get_client(); - - let current_ip = self.get_current_ipv4(&client); - - let a_records = self.get_a_records(&client, ¤t_ip); - - self.patch_records(&client, ¤t_ip, a_records); - - println!("finished"); - } - - fn get_a_records(&self, client: &Agent, current_ip: &Ipv4Addr) -> Vec { - print!("{current_ip:?}\n> listing dns A-records... "); - - match client - .get(format!( - "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", - self.zone_id - )) - .header("X-Auth-Email", &self.auth_email) - .header("Authorization", format!("Bearer {}", self.auth_key)) - .call() - { - Ok(resp) => { - match resp - .into_body() - .read_json::>() - { - Ok(resp) => { - if !resp.success { - println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); - exit(1); - } - let mut a_records = resp - .entries - .iter() - .filter(|x| x.r#type == "A") - .map(|x| x.to_owned()) - .collect::>(); - - let total_records = a_records.len(); - if total_records == 0 { - println!("none found"); - exit(0); - } - - if let Some(patterns) = self.patterns.as_ref() { - let matchers = patterns - .iter() - .map(|p| { - globset::Glob::new(p) - .expect("invalid pattern") - .compile_matcher() - }) - .collect::>(); - - a_records.retain(|x| { - let matched = matchers.iter().any(|m| m.is_match(&x.name)); - if self.invert_patterns.unwrap_or(true) { - !matched - } else { - matched - } - }); - } - - let filtered_records = a_records.len(); - if filtered_records == 0 { - println!("all records were filtered"); - exit(0); - } - - print!("{} found", total_records); - if total_records > filtered_records { - print!(", {} filtered", total_records - filtered_records); - } - - return a_records; - } - Err(e) => { - println!("failed:\n{e}"); - exit(1); - } - } - } - Err(e) => { - println!("failed:\n{e}"); - exit(1); - } - } - } - - fn patch_records( - &self, - client: &Agent, - current_ip: &Ipv4Addr, - a_records: Vec, - ) { - println!("\n> patching...",); - - let mut errors = false; - - for (i, record) in a_records.into_iter().enumerate() { - if record.ip == *current_ip { - println!("record {} ({}): up to date", i + 1, record.name); - continue; - } - print!("record {}: ", i + 1); - - match client - .patch(format!( - "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", - self.zone_id, record.id - )) - .header("X-Auth-Email", &self.auth_email) - .header("Authorization", format!("Bearer {}", self.auth_key)) - .send_json(json!({"content": current_ip.to_string()})) - { - Ok(resp) => { - if resp.status().is_success() { - println!("success"); - } else { - let status = resp.status(); - println!("failed (http {})", status); - - let data = resp - .into_body() - .read_json::>() - .unwrap_or_default(); - if !data.errors.is_empty() { - println!("error(s):\n{}", data.errors.join("\n")); - } - } - } - Err(e) => { - println!("failed: {e}"); - errors = true; - } - } - } - - if errors { - println!("finished with errors"); - exit(1); - } - } + self.ip_src + .as_ref() + .unwrap() + .iter() + .find_map(|ip| { + print!("trying {ip}: "); + + match client.get(ip).call() { + Ok(resp) => match resp.into_body().read_to_string() { + Ok(body) => match Ipv4Addr::from_str(body.trim()) { + Ok(ip_addr) => Some(ip_addr), + Err(e) => { + println!("failed: {e}"); + None + } + }, + Err(e) => { + println!("failed: {e}"); + None + } + }, + Err(e) => { + println!("failed: {e}"); + None + } + } + }) + .unwrap_or_else(|| { + println!("could not determine external ip address"); + exit(1); + }) + } + + fn run(self) { + let client = self.get_client(); + + let current_ip = self.get_current_ipv4(&client); + + let a_records = self.get_a_records(&client, ¤t_ip); + + self.patch_records(&client, ¤t_ip, a_records); + + println!("finished"); + } + + fn get_a_records(&self, client: &Agent, current_ip: &Ipv4Addr) -> Vec { + print!("{current_ip:?}\n> listing dns A-records... "); + + match client + .get(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", + self.zone_id + )) + .header("X-Auth-Email", &self.auth_email) + .header("Authorization", format!("Bearer {}", self.auth_key)) + .call() + { + Ok(resp) => { + match resp + .into_body() + .read_json::>() + { + Ok(resp) => { + if !resp.success { + println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); + exit(1); + } + let mut a_records = resp + .entries + .iter() + .filter(|x| x.r#type == "A") + .map(|x| x.to_owned()) + .collect::>(); + + let total_records = a_records.len(); + if total_records == 0 { + println!("none found"); + exit(0); + } + + if let Some(patterns) = self.patterns.as_ref() { + let matchers = patterns + .iter() + .map(|p| { + globset::Glob::new(p) + .expect("invalid pattern") + .compile_matcher() + }) + .collect::>(); + + a_records.retain(|x| { + let matched = matchers.iter().any(|m| m.is_match(&x.name)); + if self.invert_patterns.unwrap_or(true) { + !matched + } else { + matched + } + }); + } + + let filtered_records = a_records.len(); + if filtered_records == 0 { + println!("all records were filtered"); + exit(0); + } + + print!("{} found", total_records); + if total_records > filtered_records { + print!(", {} filtered", total_records - filtered_records); + } + + return a_records; + } + Err(e) => { + println!("failed:\n{e}"); + exit(1); + } + } + } + Err(e) => { + println!("failed:\n{e}"); + exit(1); + } + } + } + + fn patch_records( + &self, + client: &Agent, + current_ip: &Ipv4Addr, + a_records: Vec, + ) { + println!("\n> patching...",); + + let mut errors = false; + + for (i, record) in a_records.into_iter().enumerate() { + if record.ip == *current_ip { + println!("record {} ({}): up to date", i + 1, record.name); + continue; + } + print!("record {}: ", i + 1); + + match client + .patch(format!( + "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", + self.zone_id, record.id + )) + .header("X-Auth-Email", &self.auth_email) + .header("Authorization", format!("Bearer {}", self.auth_key)) + .send_json(json!({"content": current_ip.to_string()})) + { + Ok(resp) => { + if resp.status().is_success() { + println!("success"); + } else { + let status = resp.status(); + println!("failed (http {})", status); + + let data = resp + .into_body() + .read_json::>() + .unwrap_or_default(); + if !data.errors.is_empty() { + println!("error(s):\n{}", data.errors.join("\n")); + } + } + } + Err(e) => { + println!("failed: {e}"); + errors = true; + } + } + } + + if errors { + println!("finished with errors"); + exit(1); + } + } } From c7501b6b0570ba09266363d6b96e381e83b3b177 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 17:17:40 +0300 Subject: [PATCH 07/12] remove clone --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index db68f92..2bdcd2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ impl Default for CloudflareDDNS { } } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize)] struct CloudflareDnsResponse { success: bool, #[serde(rename = "result")] From 707012306fc94032bced7e3f98895a350074af87 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 17:24:05 +0300 Subject: [PATCH 08/12] move some prints --- src/main.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2bdcd2f..b6ac648 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,6 @@ impl CloudflareDDNS { } fn get_current_ipv4(&self, client: &Agent) -> Ipv4Addr { - println!("> getting external ipv4 address..."); self.ip_src .as_ref() .unwrap() @@ -193,18 +192,19 @@ impl CloudflareDDNS { fn run(self) { let client = self.get_client(); + println!("> getting external ipv4 address..."); let current_ip = self.get_current_ipv4(&client); - let a_records = self.get_a_records(&client, ¤t_ip); + print!("{current_ip:?}\n> listing dns A-records... "); + let a_records = self.get_a_records(&client); + println!("\n> patching...",); self.patch_records(&client, ¤t_ip, a_records); println!("finished"); } - fn get_a_records(&self, client: &Agent, current_ip: &Ipv4Addr) -> Vec { - print!("{current_ip:?}\n> listing dns A-records... "); - + fn get_a_records(&self, client: &Agent) -> Vec { match client .get(format!( "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", @@ -289,8 +289,6 @@ impl CloudflareDDNS { current_ip: &Ipv4Addr, a_records: Vec, ) { - println!("\n> patching...",); - let mut errors = false; for (i, record) in a_records.into_iter().enumerate() { From 9723f041938920ad0f6da3601caf21146d2110e1 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 17:31:59 +0300 Subject: [PATCH 09/12] add rustfmt file --- Rustfmt.toml | 2 + src/main.rs | 131 ++++++++++++++++++++------------------------------- 2 files changed, 53 insertions(+), 80 deletions(-) create mode 100644 Rustfmt.toml diff --git a/Rustfmt.toml b/Rustfmt.toml new file mode 100644 index 0000000..8d611ce --- /dev/null +++ b/Rustfmt.toml @@ -0,0 +1,2 @@ +hard_tabs = true +max_width = 130 diff --git a/src/main.rs b/src/main.rs index b6ac648..7720fe2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,13 +88,7 @@ impl CloudflareDDNS { } match Config::builder() - .set_default( - "ip_src", - DEFAULT_IPS - .into_iter() - .map(String::from) - .collect::>(), - ) + .set_default("ip_src", DEFAULT_IPS.into_iter().map(String::from).collect::>()) .unwrap() .set_default("http_timeout_s", 10) .unwrap() @@ -141,14 +135,8 @@ impl CloudflareDDNS { fn get_client(&self) -> Agent { Agent::config_builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )) - .timeout_global(Some(std::time::Duration::from_secs( - self.http_timeout_s.unwrap(), - ))) + .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) + .timeout_global(Some(std::time::Duration::from_secs(self.http_timeout_s.unwrap()))) .https_only(true) .ip_family(Ipv4Only) .build() @@ -214,68 +202,59 @@ impl CloudflareDDNS { .header("Authorization", format!("Bearer {}", self.auth_key)) .call() { - Ok(resp) => { - match resp - .into_body() - .read_json::>() - { - Ok(resp) => { - if !resp.success { - println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); - exit(1); - } - let mut a_records = resp - .entries + Ok(resp) => match resp.into_body().read_json::>() { + Ok(resp) => { + if !resp.success { + println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); + exit(1); + } + let mut a_records = resp + .entries + .iter() + .filter(|x| x.r#type == "A") + .map(|x| x.to_owned()) + .collect::>(); + + let total_records = a_records.len(); + if total_records == 0 { + println!("none found"); + exit(0); + } + + if let Some(patterns) = self.patterns.as_ref() { + let matchers = patterns .iter() - .filter(|x| x.r#type == "A") - .map(|x| x.to_owned()) + .map(|p| globset::Glob::new(p).expect("invalid pattern").compile_matcher()) .collect::>(); - let total_records = a_records.len(); - if total_records == 0 { - println!("none found"); - exit(0); - } - - if let Some(patterns) = self.patterns.as_ref() { - let matchers = patterns - .iter() - .map(|p| { - globset::Glob::new(p) - .expect("invalid pattern") - .compile_matcher() - }) - .collect::>(); - - a_records.retain(|x| { - let matched = matchers.iter().any(|m| m.is_match(&x.name)); - if self.invert_patterns.unwrap_or(true) { - !matched - } else { - matched - } - }); - } - - let filtered_records = a_records.len(); - if filtered_records == 0 { - println!("all records were filtered"); - exit(0); - } - - print!("{} found", total_records); - if total_records > filtered_records { - print!(", {} filtered", total_records - filtered_records); - } + a_records.retain(|x| { + let matched = matchers.iter().any(|m| m.is_match(&x.name)); + if self.invert_patterns.unwrap_or(true) { + !matched + } else { + matched + } + }); + } - return a_records; + let filtered_records = a_records.len(); + if filtered_records == 0 { + println!("all records were filtered"); + exit(0); } - Err(e) => { - println!("failed:\n{e}"); - exit(1); + + print!("{} found", total_records); + if total_records > filtered_records { + print!(", {} filtered", total_records - filtered_records); } + + return a_records; } - } + Err(e) => { + println!("failed:\n{e}"); + exit(1); + } + }, Err(e) => { println!("failed:\n{e}"); exit(1); @@ -283,12 +262,7 @@ impl CloudflareDDNS { } } - fn patch_records( - &self, - client: &Agent, - current_ip: &Ipv4Addr, - a_records: Vec, - ) { + fn patch_records(&self, client: &Agent, current_ip: &Ipv4Addr, a_records: Vec) { let mut errors = false; for (i, record) in a_records.into_iter().enumerate() { @@ -314,10 +288,7 @@ impl CloudflareDDNS { let status = resp.status(); println!("failed (http {})", status); - let data = resp - .into_body() - .read_json::>() - .unwrap_or_default(); + let data = resp.into_body().read_json::>().unwrap_or_default(); if !data.errors.is_empty() { println!("error(s):\n{}", data.errors.join("\n")); } From c66d1b97172a78862714671ec75e255d60aab284 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 18:22:27 +0300 Subject: [PATCH 10/12] move user agent string to constant --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 7720fe2..7a72b8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use ureq::Agent; use ureq::config::IpFamily::Ipv4Only; const DEFAULT_IPS: [&str; 2] = ["https://ipv4.icanhazip.com", "https://api.ipify.org"]; +const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); #[derive(Debug, Serialize, Deserialize)] struct CloudflareDDNS { @@ -135,7 +136,7 @@ impl CloudflareDDNS { fn get_client(&self) -> Agent { Agent::config_builder() - .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) + .user_agent(USER_AGENT) .timeout_global(Some(std::time::Duration::from_secs(self.http_timeout_s.unwrap()))) .https_only(true) .ip_family(Ipv4Only) From 7748cdd0e819fa7820fb278bfee1dd72372cd2f5 Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Sat, 10 May 2025 22:45:23 +0300 Subject: [PATCH 11/12] box dyn error --- src/main.rs | 137 +++++++++++++++++++++++++--------------------------- 1 file changed, 65 insertions(+), 72 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7a72b8c..cb4b28e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use config::Config; use serde::{Deserialize, Serialize}; use serde_json::json; +use std::error::Error; use std::io::Write; use std::net::Ipv4Addr; use std::process::exit; @@ -59,6 +60,24 @@ fn main() { } impl CloudflareDDNS { + fn run(self) { + let client = self.get_client(); + + println!("> getting external ipv4 address..."); + let current_ip = self.get_current_ipv4(&client); + + print!("{current_ip:?}\n> listing dns A-records... "); + match self.get_a_records(&client) { + Ok(a_records) => { + println!("\n> patching...",); + self.patch_records(&client, ¤t_ip, a_records); + + println!("finished"); + } + Err(e) => println!("failed getting A records:\n{e}"), + } + } + fn get_from_config() -> Self { let conf: CloudflareDDNS; let conf_dir = dirs::config_dir().unwrap().join(env!("CARGO_PKG_NAME")); @@ -178,89 +197,63 @@ impl CloudflareDDNS { }) } - fn run(self) { - let client = self.get_client(); - - println!("> getting external ipv4 address..."); - let current_ip = self.get_current_ipv4(&client); - - print!("{current_ip:?}\n> listing dns A-records... "); - let a_records = self.get_a_records(&client); - - println!("\n> patching...",); - self.patch_records(&client, ¤t_ip, a_records); - - println!("finished"); - } - - fn get_a_records(&self, client: &Agent) -> Vec { - match client + fn get_a_records(&self, client: &Agent) -> Result, Box> { + let resp = client .get(format!( "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A", self.zone_id )) .header("X-Auth-Email", &self.auth_email) .header("Authorization", format!("Bearer {}", self.auth_key)) - .call() - { - Ok(resp) => match resp.into_body().read_json::>() { - Ok(resp) => { - if !resp.success { - println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); - exit(1); - } - let mut a_records = resp - .entries - .iter() - .filter(|x| x.r#type == "A") - .map(|x| x.to_owned()) - .collect::>(); - - let total_records = a_records.len(); - if total_records == 0 { - println!("none found"); - exit(0); - } - - if let Some(patterns) = self.patterns.as_ref() { - let matchers = patterns - .iter() - .map(|p| globset::Glob::new(p).expect("invalid pattern").compile_matcher()) - .collect::>(); - - a_records.retain(|x| { - let matched = matchers.iter().any(|m| m.is_match(&x.name)); - if self.invert_patterns.unwrap_or(true) { - !matched - } else { - matched - } - }); - } + .call()?; - let filtered_records = a_records.len(); - if filtered_records == 0 { - println!("all records were filtered"); - exit(0); - } + let resp = resp.into_body().read_json::>()?; - print!("{} found", total_records); - if total_records > filtered_records { - print!(", {} filtered", total_records - filtered_records); - } + if !resp.success { + println!("cloudflare api error(s):\n{}", resp.errors.join("\n")); + exit(1); + } + let mut a_records = resp + .entries + .iter() + .filter(|x| x.r#type == "A") + .map(|x| x.to_owned()) + .collect::>(); + + let total_records = a_records.len(); + if total_records == 0 { + println!("none found"); + exit(0); + } - return a_records; - } - Err(e) => { - println!("failed:\n{e}"); - exit(1); + if let Some(patterns) = self.patterns.as_ref() { + let matchers = patterns + .iter() + .map(|p| globset::Glob::new(p).expect("invalid pattern").compile_matcher()) + .collect::>(); + + a_records.retain(|x| { + let matched = matchers.iter().any(|m| m.is_match(&x.name)); + if self.invert_patterns.unwrap_or(true) { + !matched + } else { + matched } - }, - Err(e) => { - println!("failed:\n{e}"); - exit(1); - } + }); } + + let filtered_records = a_records.len(); + if filtered_records == 0 { + println!("all records were filtered"); + exit(0); + } + + print!("{} found", total_records); + if total_records > filtered_records { + print!(", {} filtered", total_records - filtered_records); + } + + return Ok(a_records); } fn patch_records(&self, client: &Agent, current_ip: &Ipv4Addr, a_records: Vec) { From 651995fed2405a9f8a955e8d7a90b754741f942c Mon Sep 17 00:00:00 2001 From: Kiisu_Master <142301391+Kiisu-Master@users.noreply.github.com> Date: Fri, 16 May 2025 20:27:01 +0300 Subject: [PATCH 12/12] add exit code --- src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index cb4b28e..aebe34b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,7 +74,10 @@ impl CloudflareDDNS { println!("finished"); } - Err(e) => println!("failed getting A records:\n{e}"), + Err(e) => { + println!("failed getting A records:\n{e}"); + exit(1); + } } }