mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-09-20 14:39:04 -03:00
Release v2.2.0
- Add native Zulip notification support via zulip:// shoutrrr URLs (#271) - Add ?messagekey= option for generic webhooks to rename the JSON payload field (#271) - Change DELETE_ON_FAILURE default to false: preserve existing DNS records and skip WAF list updates when IP detection fails (#277) - Document the local.iface.stable IPv6 provider and Helm chart added since v2.1.2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
104
src/config.rs
104
src/config.rs
@@ -1,7 +1,7 @@
|
||||
use crate::cloudflare::{Auth, TTL, WAFList};
|
||||
use crate::cloudflare::{Auth, WAFList, TTL};
|
||||
use crate::domain;
|
||||
use crate::notifier::{
|
||||
CompositeNotifier, Heartbeat, HeartbeatMonitor, HealthchecksMonitor, NotifierDyn,
|
||||
CompositeNotifier, HealthchecksMonitor, Heartbeat, HeartbeatMonitor, NotifierDyn,
|
||||
ShoutrrrNotifier, UptimeKumaMonitor,
|
||||
};
|
||||
use crate::pp::{self, PP};
|
||||
@@ -130,9 +130,15 @@ impl CronSchedule {
|
||||
fn parse_duration_string(s: &str) -> Option<Duration> {
|
||||
let s = s.trim();
|
||||
if let Some(minutes) = s.strip_suffix('m') {
|
||||
minutes.parse::<u64>().ok().map(|m| Duration::from_secs(m * 60))
|
||||
minutes
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.map(|m| Duration::from_secs(m * 60))
|
||||
} else if let Some(hours) = s.strip_suffix('h') {
|
||||
hours.parse::<u64>().ok().map(|h| Duration::from_secs(h * 3600))
|
||||
hours
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.map(|h| Duration::from_secs(h * 3600))
|
||||
} else if let Some(secs) = s.strip_suffix('s') {
|
||||
secs.parse::<u64>().ok().map(Duration::from_secs)
|
||||
} else {
|
||||
@@ -146,7 +152,10 @@ fn parse_duration_string(s: &str) -> Option<Duration> {
|
||||
// ============================================================
|
||||
|
||||
fn getenv(key: &str) -> Option<String> {
|
||||
env::var(key).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
|
||||
env::var(key)
|
||||
.ok()
|
||||
.map(|v| v.trim().to_string())
|
||||
.filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
fn getenv_bool(key: &str, default: bool) -> bool {
|
||||
@@ -187,7 +196,10 @@ fn read_auth_from_env(ppfmt: &PP) -> Option<Auth> {
|
||||
val
|
||||
}) {
|
||||
if token == "YOUR-CLOUDFLARE-API-TOKEN" {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, "Please set CLOUDFLARE_API_TOKEN to your actual API token");
|
||||
ppfmt.errorf(
|
||||
pp::EMOJI_ERROR,
|
||||
"Please set CLOUDFLARE_API_TOKEN to your actual API token",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
return Some(Auth::Token(token));
|
||||
@@ -212,7 +224,10 @@ fn read_auth_from_env(ppfmt: &PP) -> Option<Auth> {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("Failed to read API token file '{path}': {e}"));
|
||||
ppfmt.errorf(
|
||||
pp::EMOJI_ERROR,
|
||||
&format!("Failed to read API token file '{path}': {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,27 +249,31 @@ fn read_providers_from_env(ppfmt: &PP) -> Result<HashMap<IpType, ProviderType>,
|
||||
let ip4_str = getenv("IP4_PROVIDER").or_else(|| {
|
||||
let val = getenv("IP4_POLICY");
|
||||
if val.is_some() {
|
||||
ppfmt.warningf(pp::EMOJI_WARNING, "IP4_POLICY is deprecated; use IP4_PROVIDER instead");
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
"IP4_POLICY is deprecated; use IP4_PROVIDER instead",
|
||||
);
|
||||
}
|
||||
val
|
||||
});
|
||||
let ip6_str = getenv("IP6_PROVIDER").or_else(|| {
|
||||
let val = getenv("IP6_POLICY");
|
||||
if val.is_some() {
|
||||
ppfmt.warningf(pp::EMOJI_WARNING, "IP6_POLICY is deprecated; use IP6_PROVIDER instead");
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
"IP6_POLICY is deprecated; use IP6_PROVIDER instead",
|
||||
);
|
||||
}
|
||||
val
|
||||
});
|
||||
|
||||
let ip4_provider = match ip4_str {
|
||||
Some(s) => ProviderType::parse(&s)
|
||||
.map_err(|e| format!("Invalid IP4_PROVIDER: {e}"))?,
|
||||
Some(s) => ProviderType::parse(&s).map_err(|e| format!("Invalid IP4_PROVIDER: {e}"))?,
|
||||
None => ProviderType::CloudflareTrace { url: None },
|
||||
};
|
||||
|
||||
let ip6_provider = match ip6_str {
|
||||
Some(s) => ProviderType::parse(&s)
|
||||
.map_err(|e| format!("Invalid IP6_PROVIDER: {e}"))?,
|
||||
Some(s) => ProviderType::parse(&s).map_err(|e| format!("Invalid IP6_PROVIDER: {e}"))?,
|
||||
None => ProviderType::CloudflareTrace { url: None },
|
||||
};
|
||||
|
||||
@@ -392,7 +411,11 @@ pub fn parse_legacy_config(content: &str) -> Result<LegacyConfig, String> {
|
||||
}
|
||||
|
||||
/// Convert a legacy config into a unified AppConfig
|
||||
fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Result<AppConfig, String> {
|
||||
fn legacy_to_app_config(
|
||||
legacy: LegacyConfig,
|
||||
dry_run: bool,
|
||||
repeat: bool,
|
||||
) -> Result<AppConfig, String> {
|
||||
// Extract auth from first entry
|
||||
let auth = if let Some(entry) = legacy.cloudflare.first() {
|
||||
if !entry.authentication.api_token.is_empty()
|
||||
@@ -450,7 +473,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Re
|
||||
update_cron: schedule,
|
||||
update_on_start: true,
|
||||
delete_on_stop: false,
|
||||
delete_on_failure: true,
|
||||
delete_on_failure: false,
|
||||
ttl,
|
||||
proxied_expression: None,
|
||||
record_comment: None,
|
||||
@@ -505,7 +528,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
||||
let update_cron = read_cron_from_env(ppfmt)?;
|
||||
let update_on_start = getenv_bool("UPDATE_ON_START", true);
|
||||
let delete_on_stop = getenv_bool("DELETE_ON_STOP", false);
|
||||
let delete_on_failure = getenv_bool("DELETE_ON_FAILURE", true);
|
||||
let delete_on_failure = getenv_bool("DELETE_ON_FAILURE", false);
|
||||
|
||||
let ttl_val = getenv("TTL")
|
||||
.and_then(|s| s.parse::<i64>().ok())
|
||||
@@ -620,11 +643,17 @@ pub fn setup_notifiers(ppfmt: &PP) -> CompositeNotifier {
|
||||
if !shoutrrr_urls.is_empty() {
|
||||
match ShoutrrrNotifier::new(&shoutrrr_urls) {
|
||||
Ok(n) => {
|
||||
ppfmt.infof(pp::EMOJI_NOTIFY, &format!("Notifications: {}", n.describe()));
|
||||
ppfmt.infof(
|
||||
pp::EMOJI_NOTIFY,
|
||||
&format!("Notifications: {}", n.describe()),
|
||||
);
|
||||
notifiers.push(Box::new(n));
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("Failed to setup notifications: {e}"));
|
||||
ppfmt.errorf(
|
||||
pp::EMOJI_ERROR,
|
||||
&format!("Failed to setup notifications: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,7 +692,10 @@ pub fn print_config_summary(config: &AppConfig, ppfmt: &PP) {
|
||||
if !config.domains.is_empty() {
|
||||
ppfmt.noticef(pp::EMOJI_CONFIG, "Domains to update:");
|
||||
for (ip_type, domains) in &config.domains {
|
||||
inner.noticef("", &format!("{}: {}", ip_type.describe(), domains.join(", ")));
|
||||
inner.noticef(
|
||||
"",
|
||||
&format!("{}: {}", ip_type.describe(), domains.join(", ")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +707,10 @@ pub fn print_config_summary(config: &AppConfig, ppfmt: &PP) {
|
||||
}
|
||||
|
||||
for (ip_type, provider) in &config.providers {
|
||||
inner.infof("", &format!("{} provider: {}", ip_type.describe(), provider.name()));
|
||||
inner.infof(
|
||||
"",
|
||||
&format!("{} provider: {}", ip_type.describe(), provider.name()),
|
||||
);
|
||||
}
|
||||
|
||||
inner.infof("", &format!("TTL: {}", config.ttl.describe()));
|
||||
@@ -686,7 +721,10 @@ pub fn print_config_summary(config: &AppConfig, ppfmt: &PP) {
|
||||
}
|
||||
|
||||
if !config.reject_cloudflare_ips {
|
||||
inner.warningf("", "Cloudflare IP rejection: DISABLED (REJECT_CLOUDFLARE_IPS=false)");
|
||||
inner.warningf(
|
||||
"",
|
||||
"Cloudflare IP rejection: DISABLED (REJECT_CLOUDFLARE_IPS=false)",
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref comment) = config.record_comment {
|
||||
@@ -766,7 +804,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration_string_whitespace() {
|
||||
assert_eq!(parse_duration_string(" 5m "), Some(Duration::from_secs(300)));
|
||||
assert_eq!(
|
||||
parse_duration_string(" 5m "),
|
||||
Some(Duration::from_secs(300))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -962,7 +1003,10 @@ mod tests {
|
||||
std::env::remove_var("IP6_DOMAINS");
|
||||
let pp = PP::new(false, false);
|
||||
let domains = read_domains_from_env(&pp);
|
||||
assert_eq!(domains.get(&IpType::V4).unwrap(), &vec!["v4.example.com".to_string()]);
|
||||
assert_eq!(
|
||||
domains.get(&IpType::V4).unwrap(),
|
||||
&vec!["v4.example.com".to_string()]
|
||||
);
|
||||
assert!(domains.get(&IpType::V6).is_none());
|
||||
std::env::remove_var("IP4_DOMAINS");
|
||||
}
|
||||
@@ -1049,7 +1093,9 @@ mod tests {
|
||||
ip6_provider: None,
|
||||
};
|
||||
let config = legacy_to_app_config(legacy, true, true).unwrap();
|
||||
assert!(matches!(config.update_cron, CronSchedule::Every(d) if d == Duration::from_secs(120)));
|
||||
assert!(
|
||||
matches!(config.update_cron, CronSchedule::Every(d) if d == Duration::from_secs(120))
|
||||
);
|
||||
assert!(config.repeat);
|
||||
assert!(config.dry_run);
|
||||
}
|
||||
@@ -1102,7 +1148,10 @@ mod tests {
|
||||
};
|
||||
let config = legacy_to_app_config(legacy, false, false).unwrap();
|
||||
assert!(matches!(config.providers[&IpType::V4], ProviderType::Ipify));
|
||||
assert!(matches!(config.providers[&IpType::V6], ProviderType::CloudflareDOH));
|
||||
assert!(matches!(
|
||||
config.providers[&IpType::V6],
|
||||
ProviderType::CloudflareDOH
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1395,7 +1444,10 @@ mod tests {
|
||||
fn set(key: &str, value: &str) -> Self {
|
||||
let lock = ENV_MUTEX.lock().unwrap();
|
||||
std::env::set_var(key, value);
|
||||
Self { keys: vec![key.to_string()], _lock: lock }
|
||||
Self {
|
||||
keys: vec![key.to_string()],
|
||||
_lock: lock,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, key: &str, value: &str) {
|
||||
|
||||
Reference in New Issue
Block a user