Compare commits
2 Commits
b22616e209
...
b6f447e39f
Author | SHA1 | Date | |
---|---|---|---|
b6f447e39f | |||
8e5d472018 |
@ -1,21 +1,26 @@
|
|||||||
// SPDX: BSD-2-Clause
|
// SPDX: BSD-2-Clause
|
||||||
|
|
||||||
|
use crate::utils::duration_to_string;
|
||||||
use anyhow;
|
use anyhow;
|
||||||
use dirs;
|
use dirs;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde::{self, Deserialize, Serialize};
|
use serde::{self, Deserialize, Serialize};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::{fs, io::AsyncReadExt};
|
use tokio::{fs, io::AsyncReadExt};
|
||||||
|
|
||||||
use crate::PROGRAM_NAME;
|
use crate::PROGRAM_NAME;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub zone_id: Box<str>,
|
pub zone_id: String,
|
||||||
pub api_key: Box<str>,
|
pub api_key: String,
|
||||||
pub domains: Vec<Box<str>>,
|
pub domains: Vec<Box<str>>,
|
||||||
|
#[serde(default)]
|
||||||
pub max_errors_in_row: Option<usize>,
|
pub max_errors_in_row: Option<usize>,
|
||||||
|
#[serde(with = "duration_format", default)]
|
||||||
|
pub max_duration: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
|
pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
|
||||||
@ -67,3 +72,58 @@ pub async fn read_config<P: AsRef<Path>>(path: &P) -> anyhow::Result<Config> {
|
|||||||
file.read_to_string(&mut buf).await?;
|
file.read_to_string(&mut buf).await?;
|
||||||
Ok(toml::from_str(&buf)?)
|
Ok(toml::from_str(&buf)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod duration_format {
|
||||||
|
use super::*;
|
||||||
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match duration.as_ref() {
|
||||||
|
Some(duration) => serializer.serialize_str(duration_to_string(duration).trim()),
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Option::<Box<str>>::deserialize(deserializer)?.map_or(Ok(None), |s| {
|
||||||
|
parse_duration(&s)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_duration(s: &str) -> Result<Duration, String> {
|
||||||
|
let mut total_duration = Duration::new(0, 0);
|
||||||
|
let units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)];
|
||||||
|
let mut remainder = s;
|
||||||
|
|
||||||
|
for &(unit, factor) in &units {
|
||||||
|
if let Some(idx) = remainder.find(unit) {
|
||||||
|
let (value, rest) = remainder.split_at(idx);
|
||||||
|
let value: u64 = value.trim().parse().map_err(|_| "Invalid number")?;
|
||||||
|
total_duration += Duration::from_secs(value * factor);
|
||||||
|
remainder = &rest[unit.len()..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(idx) = remainder.find("ns") {
|
||||||
|
let (value, rest) = remainder.split_at(idx);
|
||||||
|
let value: u32 = value.trim().parse().map_err(|_| "Invalid number")?;
|
||||||
|
total_duration += Duration::new(0, value);
|
||||||
|
remainder = &rest["ns".len()..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if !remainder.trim().is_empty() {
|
||||||
|
return Err("Invalid duration format".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total_duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
mod cloudflare;
|
mod cloudflare;
|
||||||
mod config;
|
mod config;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod public_ip;
|
|
||||||
mod message_handler;
|
mod message_handler;
|
||||||
|
mod public_ip;
|
||||||
|
mod tests;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub use cloudflare::CloudflareClient;
|
pub use cloudflare::CloudflareClient;
|
||||||
pub use config::{get_config_path, read_config, Config};
|
pub use config::{get_config_path, read_config, Config};
|
||||||
pub use logging::init_logger;
|
pub use logging::init_logger;
|
||||||
pub use public_ip::get_current_public_ipv4;
|
|
||||||
pub use message_handler::MessageHandler;
|
pub use message_handler::MessageHandler;
|
||||||
|
pub use public_ip::get_current_public_ipv4;
|
||||||
|
|
||||||
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
|
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
|
||||||
pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10;
|
pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10;
|
||||||
|
29
src/main.rs
29
src/main.rs
@ -1,5 +1,6 @@
|
|||||||
// SPDX: BSD-2-Clause
|
// SPDX: BSD-2-Clause
|
||||||
|
|
||||||
|
use futures::future::{self, Either};
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
@ -10,9 +11,12 @@ use log::{error, info};
|
|||||||
use netlink_sys::{AsyncSocket, SocketAddr};
|
use netlink_sys::{AsyncSocket, SocketAddr};
|
||||||
use rtnetlink::new_connection;
|
use rtnetlink::new_connection;
|
||||||
|
|
||||||
use dynip_cloudflare::{utils, CloudflareClient, MessageHandler, MAX_ERORS_IN_ROW_DEFAULT};
|
use dynip_cloudflare::{
|
||||||
|
utils::{self, duration_to_string},
|
||||||
|
CloudflareClient, MessageHandler, MAX_ERORS_IN_ROW_DEFAULT,
|
||||||
|
};
|
||||||
use scopeguard::defer;
|
use scopeguard::defer;
|
||||||
use tokio::{signal, sync::Notify};
|
use tokio::{signal, sync::Notify, time};
|
||||||
|
|
||||||
const RTNLGRP_LINK: u32 = 1;
|
const RTNLGRP_LINK: u32 = 1;
|
||||||
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
||||||
@ -81,11 +85,32 @@ async fn main() {
|
|||||||
config.max_errors_in_row.unwrap_or(MAX_ERORS_IN_ROW_DEFAULT),
|
config.max_errors_in_row.unwrap_or(MAX_ERORS_IN_ROW_DEFAULT),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut interval = config.max_duration.map(|duration| {
|
||||||
|
let mut interval = time::interval(duration);
|
||||||
|
interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
|
||||||
|
interval
|
||||||
|
});
|
||||||
|
|
||||||
while !should_exit.load(Ordering::SeqCst) {
|
while !should_exit.load(Ordering::SeqCst) {
|
||||||
|
let tick_future = match interval.as_mut() {
|
||||||
|
Some(interval) => Either::Left(interval.tick()),
|
||||||
|
None => Either::Right(future::pending::<tokio::time::Instant>()),
|
||||||
|
};
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = notify.notified() => break,
|
_ = notify.notified() => break,
|
||||||
|
_ = tick_future => {
|
||||||
|
if let Some(duration) = config.max_duration.as_ref() {
|
||||||
|
let duration_string = duration_to_string(duration);
|
||||||
|
let log_string = format!("{} has passed since last check, checking...", duration_string.trim());
|
||||||
|
message_handler.log_and_check(Some(&log_string), Option::<&&str>::None).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
message = messages.next() => {
|
message = messages.next() => {
|
||||||
if let Some((message, _)) = message {
|
if let Some((message, _)) = message {
|
||||||
|
if let Some(interval) = interval.as_mut() {
|
||||||
|
interval.reset();
|
||||||
|
}
|
||||||
message_handler.handle_message(message).await;
|
message_handler.handle_message(message).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// SPDX: BSD-2-Clause
|
// SPDX: BSD-2-Clause
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use netlink_packet_core::{NetlinkMessage, NetlinkPayload};
|
use netlink_packet_core::{NetlinkMessage, NetlinkPayload};
|
||||||
use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
|
use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
|
||||||
@ -34,13 +33,14 @@ where
|
|||||||
pub async fn handle_message(&mut self, message: NetlinkMessage<RtnlMessage>) -> Option<()> {
|
pub async fn handle_message(&mut self, message: NetlinkMessage<RtnlMessage>) -> Option<()> {
|
||||||
match message.payload {
|
match message.payload {
|
||||||
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
|
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
|
||||||
self.log_and_check("New IPv4 address", &msg).await
|
self.log_and_check(Some("New IPv4 address"), Some(&msg))
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => {
|
NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => {
|
||||||
self.log_info("Deleted IPv4 address", &msg).await
|
self.log_info("Deleted IPv4 address", &msg).await
|
||||||
}
|
}
|
||||||
NetlinkPayload::InnerMessage(RtnlMessage::NewLink(link)) => {
|
NetlinkPayload::InnerMessage(RtnlMessage::NewLink(link)) => {
|
||||||
self.log_and_check("New link (interface connected)", &link)
|
self.log_and_check(Some("New link (interface connected)"), Some(&link))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
|
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
|
||||||
@ -54,13 +54,21 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn log_and_check<D, M>(&mut self, log_msg: &D, msg: &M) -> Option<()>
|
pub async fn log_and_check<D, M>(&mut self, log_msg: Option<&D>, msg: Option<&M>) -> Option<()>
|
||||||
where
|
where
|
||||||
D: fmt::Display + ?Sized,
|
D: fmt::Display + ?Sized,
|
||||||
M: fmt::Debug,
|
M: fmt::Debug,
|
||||||
{
|
{
|
||||||
info!("{}", log_msg);
|
if let Some(s) = log_msg {
|
||||||
debug!("{}: {:?}", log_msg, msg);
|
info!("{}", s);
|
||||||
|
}
|
||||||
|
if let Some(m) = msg {
|
||||||
|
if let Some(lm) = log_msg {
|
||||||
|
debug!("{}: {:?}", lm, m);
|
||||||
|
} else {
|
||||||
|
debug!("{:?}", m);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Err(e) = self.cloudflare.check().await {
|
if let Err(e) = self.cloudflare.check().await {
|
||||||
self.errs_counter += 1;
|
self.errs_counter += 1;
|
||||||
error!(
|
error!(
|
||||||
|
75
src/tests/config_serialization.rs
Normal file
75
src/tests/config_serialization.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
use toml;
|
||||||
|
use crate::Config;
|
||||||
|
|
||||||
|
const TOML_STR_ONE: &str = r#"
|
||||||
|
zone_id = ""
|
||||||
|
api_key = ""
|
||||||
|
domains = [""]
|
||||||
|
max_duration = "1d 2h 30m 45s 500000000ns"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize() {
|
||||||
|
let config: Config = toml::from_str(TOML_STR_ONE).unwrap();
|
||||||
|
assert_eq!(config.max_duration, Some(Duration::new(95445, 500000000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize() {
|
||||||
|
let config = Config {
|
||||||
|
zone_id: "".into(),
|
||||||
|
api_key: "".into(),
|
||||||
|
domains: vec!["".into()],
|
||||||
|
max_errors_in_row: None,
|
||||||
|
max_duration: Some(Duration::new(95445, 500000000)),
|
||||||
|
};
|
||||||
|
let toml_str = toml::to_string(&config).unwrap();
|
||||||
|
assert_eq!(TOML_STR_ONE.trim(), toml_str.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_none() {
|
||||||
|
let toml_str = r#"
|
||||||
|
zone_id = ""
|
||||||
|
api_key = ""
|
||||||
|
domains = [""]
|
||||||
|
max_errors_in_row = 5
|
||||||
|
"#;
|
||||||
|
let config: Config = toml::from_str(toml_str).unwrap();
|
||||||
|
assert_eq!(config.max_duration, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_none() {
|
||||||
|
let toml_to_be = r#"
|
||||||
|
zone_id = ""
|
||||||
|
api_key = ""
|
||||||
|
domains = [""]
|
||||||
|
"#;
|
||||||
|
let config = Config {
|
||||||
|
zone_id: "".into(),
|
||||||
|
api_key: "".into(),
|
||||||
|
domains: vec!["".into()],
|
||||||
|
max_errors_in_row: None,
|
||||||
|
max_duration: None,
|
||||||
|
};
|
||||||
|
let toml_str = toml::to_string(&config).unwrap();
|
||||||
|
assert_eq!(toml_to_be.trim(), toml_str.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let toml_str = r#"
|
||||||
|
zone_id = ""
|
||||||
|
api_key = ""
|
||||||
|
domains = [""]
|
||||||
|
max_errors_in_row = 5
|
||||||
|
max_duration = "1d 2h 30m 45s 500000000ns"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config: Config = toml::from_str(toml_str).unwrap();
|
||||||
|
println!("{:?}", config);
|
||||||
|
|
||||||
|
let toml_out = toml::to_string(&config).unwrap();
|
||||||
|
println!("{}", toml_out);
|
||||||
|
}
|
2
src/tests/mod.rs
Normal file
2
src/tests/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod config_serialization;
|
31
src/utils.rs
31
src/utils.rs
@ -1,5 +1,7 @@
|
|||||||
// SPDX: BSD-2-Clause
|
// SPDX: BSD-2-Clause
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{get_config_path, read_config, Config};
|
use crate::{get_config_path, read_config, Config};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
@ -28,3 +30,32 @@ pub async fn get_config() -> Option<Config> {
|
|||||||
}
|
}
|
||||||
read_result.ok()
|
read_result.ok()
|
||||||
}
|
}
|
||||||
|
pub fn duration_to_string(duration: &Duration) -> String {
|
||||||
|
let mut secs = duration.as_secs();
|
||||||
|
let nanos = duration.subsec_nanos();
|
||||||
|
|
||||||
|
let days = secs / 86400;
|
||||||
|
secs %= 86400;
|
||||||
|
let hours = secs / 3600;
|
||||||
|
secs %= 3600;
|
||||||
|
let minutes = secs / 60;
|
||||||
|
secs %= 60;
|
||||||
|
|
||||||
|
let mut ret = String::new();
|
||||||
|
if days > 0 {
|
||||||
|
ret.push_str(&format!("{}d ", days));
|
||||||
|
}
|
||||||
|
if hours > 0 {
|
||||||
|
ret.push_str(&format!("{}h ", hours));
|
||||||
|
}
|
||||||
|
if minutes > 0 {
|
||||||
|
ret.push_str(&format!("{}m ", minutes));
|
||||||
|
}
|
||||||
|
if secs > 0 {
|
||||||
|
ret.push_str(&format!("{}s ", secs));
|
||||||
|
}
|
||||||
|
if nanos > 0 {
|
||||||
|
ret.push_str(&format!("{}ns", nanos));
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user