Compare commits

..

4 Commits

Author SHA1 Message Date
f5d7e05e9f simplify 2024-07-14 16:10:13 +02:00
b8181d2e89 break out message handler 2024-07-14 16:02:28 +02:00
ba401ccdc5 break out massive loop 2024-07-14 15:54:15 +02:00
e535c66b50 logging and other stuff 2024-07-14 15:40:59 +02:00
19 changed files with 136 additions and 548 deletions

2
.gitignore vendored
View File

@ -1,3 +1 @@
/target /target
logs
dynip-cloudflare.toml

24
LICENSE
View File

@ -1,24 +0,0 @@
BSD 2-Clause License
Copyright (c) 2024, Love Billenius
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,80 +0,0 @@
# Dynamic IP Updater with Cloudflare DNS
This project is a dynamic IP updater for Cloudflare DNS records. It listens for changes in the IPv4 address of the network interface and updates the DNS records in Cloudflare accordingly.
## Features
- Automatically detects changes in the public IP address.
- Updates Cloudflare DNS records when the public IP changes.
- Listens for network interface events to detect IP changes.
- Handles errors and retries updates to Cloudflare.
## Prerequisites
- Rust and Cargo installed.
- Cloudflare account with API token.
- Network interface with dynamic IP address.
## Installation
1. **Clone the repository:**
```sh
git clone https://github.com/yourusername/dynip-cloudflare.git
cd dynip-cloudflare
```
2. **Build the project:**
```sh
cargo build --release
```
3. **Create configuration file:**
Create a `config.toml` file in the current directory or in the configuration directory (typically `~/.config/dynip-cloudflare/`) with the following content:
```toml
zone_id = "your_zone_id"
api_key = "your_api_key"
domains = ["example.com", "sub.example.com"]
max_errors_in_row = 5
```
## Usage
1. **Run the program:**
```sh
./target/release/dynip-cloudflare
```
2. **Logging:**
The program logs to both stderr and a rotating file located in the `logs` directory. The log level is set to `Info`.
## Configuration
### `config.toml`
- `zone_id`: The Zone ID of your Cloudflare domain.
- `api_key`: The API key for your Cloudflare account.
- `domains`: A list of domain names to update.
- `max_errors_in_row`: Maximum number of consecutive errors before the program stops. Will be set to 10 as default
### Example
```toml
zone_id = "your_zone_id"
api_key = "your_api_key"
domains = ["example.com", "sub.example.com"]
max_errors_in_row = 5
```
## Error Handling
The program handles errors gracefully, with retries and logging of error messages. If the number of consecutive errors exceeds `max_errors_in_row`, the program will stop.
## License
This project is licensed under the MIT License.

View File

@ -0,0 +1,5 @@
2024-07-14 15:30:54 [ERROR] Failed to find any config file, tried the paths: /home/love/Documents/Code/dynip-cloudflare/dynip-cloudflare.toml, /home/love/.config/dynip-cloudflare/config.toml
2024-07-14 15:35:23 [ERROR] Failed to find any config file, tried the paths: /home/love/Documents/Code/dynip-cloudflare/dynip-cloudflare.toml, /home/love/.config/dynip-cloudflare/config.toml
2024-07-14 15:38:03 [ERROR] Failed to find any config file, tried the paths: /home/love/Documents/Code/dynip-cloudflare/dynip-cloudflare.toml, /home/love/.config/dynip-cloudflare/config.toml
2024-07-14 15:38:18 [ERROR] Failed to find any config file, tried the paths: /home/love/Documents/Code/dynip-cloudflare/dynip-cloudflare.toml, /home/love/.config/dynip-cloudflare/config.toml
2024-07-14 15:40:38 [ERROR] Failed to find any config file, tried the paths: /home/love/Documents/Code/dynip-cloudflare/dynip-cloudflare.toml, /home/love/.config/dynip-cloudflare/config.toml

View File

@ -0,0 +1 @@
2024-07-14 15:26:30 [ERROR] Failed to find any config file, tried the paths: /home/love/Documents/Code/dynip-cloudflare/dynip-cloudflare.toml, /home/love/.config/dynip-cloudflare/config.toml

View File

@ -1,34 +1,41 @@
// SPDX: BSD-2-Clause
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::{error, info}; use log::{error, info};
use reqwest::Client; use reqwest::Client;
use serde::{self, Deserialize, Serialize};
use std::{ use std::{
fmt, collections::HashMap,
net::{IpAddr, Ipv4Addr}, net::{IpAddr, Ipv4Addr},
}; };
use super::cloudflare_responses::{CloudflareResponse, DnsRecord};
use crate::get_current_public_ipv4; use crate::get_current_public_ipv4;
pub struct CloudflareClient<A, Z> pub struct CloudflareClient {
where
A: fmt::Display,
Z: fmt::Display,
{
client: Client, client: Client,
domains: Vec<Box<str>>, domains: Vec<Box<str>>,
current_ip: Ipv4Addr, current_ip: Ipv4Addr,
api_key: A, api_key: Box<str>,
zone_id: Z, zone_id: Box<str>,
} }
impl<Api, Zone> CloudflareClient<Api, Zone> #[derive(Serialize, Deserialize, Clone, Debug)]
where struct DnsRecord {
Api: fmt::Display, id: String,
Zone: fmt::Display, #[serde(rename = "type")]
{ record_type: Box<str>,
pub async fn new(api_key: Api, zone_id: Zone, domains: Vec<Box<str>>) -> Result<Self> { name: Box<str>,
content: Box<str>,
ttl: u32,
proxied: bool,
locked: bool,
zone_id: Box<str>,
zone_name: Box<str>,
modified_on: Box<str>,
created_on: Box<str>,
meta: HashMap<Box<str>, serde_json::Value>,
}
impl CloudflareClient {
pub async fn new(api_key: Box<str>, zone_id: Box<str>, domains: Vec<Box<str>>) -> Result<Self> {
let force_ipv4 = IpAddr::from([0, 0, 0, 0]); let force_ipv4 = IpAddr::from([0, 0, 0, 0]);
let client = reqwest::ClientBuilder::new() let client = reqwest::ClientBuilder::new()
.local_address(force_ipv4) .local_address(force_ipv4)
@ -50,11 +57,10 @@ where
pub async fn check(&mut self) -> Result<()> { pub async fn check(&mut self) -> Result<()> {
let new_ip = get_current_public_ipv4(&self.client).await?; let new_ip = get_current_public_ipv4(&self.client).await?;
if new_ip == self.current_ip { if new_ip == self.current_ip {
info!("IP '{}' is already set", new_ip);
return Ok(()); return Ok(());
} }
info!( info!(
"IP has changed from '{}' -> '{}'", "Ip has changed from '{}' -> '{}'",
&self.current_ip, &new_ip &self.current_ip, &new_ip
); );
self.update_dns_records(new_ip).await?; self.update_dns_records(new_ip).await?;
@ -69,7 +75,7 @@ where
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
error!( error!(
"Could not fetch DNS records for domain '{}': {:?}", "Could not getch dns records for domain '{}': {:?}",
&domain, &e &domain, &e
); );
continue; continue;
@ -79,10 +85,6 @@ where
let new_ip_s = new_ip.to_string().into_boxed_str(); let new_ip_s = new_ip.to_string().into_boxed_str();
for mut record in records.into_iter() { for mut record in records.into_iter() {
if record.content == new_ip_s { if record.content == new_ip_s {
info!(
"On {}, ip already updated to '{}'",
&record.name, &record.content
);
continue; continue;
} }
info!( info!(
@ -102,13 +104,12 @@ where
Ok(()) Ok(())
} }
async fn get_dns_records<D: fmt::Display>(&self, domain: &D) -> Result<Vec<DnsRecord>> { async fn get_dns_records(&self, domain: &str) -> Result<Vec<DnsRecord>> {
let url = format!( let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A&name={}", "https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A&name={}",
self.zone_id, domain self.zone_id, domain
); );
let mut response = self
let response = self
.client .client
.get(&url) .get(&url)
.header("Authorization", format!("Bearer {}", self.api_key)) .header("Authorization", format!("Bearer {}", self.api_key))
@ -116,23 +117,12 @@ where
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?
.text() .json::<HashMap<String, Vec<DnsRecord>>>()
.await?; .await?;
let cloudflare_response: CloudflareResponse = response
serde_json::from_str(&response).context("Failed to deserialize Cloudflare response")?; .remove("result")
.context("Key result not in return")
if !cloudflare_response.success {
error!(
"Cloudflare API returned errors: {:?}",
cloudflare_response.errors
);
return Err(anyhow::anyhow!("Cloudflare API returned errors"));
}
cloudflare_response
.result
.context("Key 'result' not found in response")
} }
async fn update_dns_record(&self, record: &mut DnsRecord, ip_content: Box<str>) -> Result<()> { async fn update_dns_record(&self, record: &mut DnsRecord, ip_content: Box<str>) -> Result<()> {

View File

@ -1,26 +1,18 @@
// 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, 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(Debug, Deserialize, Serialize)] #[derive(Deserialize, Serialize, Debug)]
pub struct Config { pub struct Config {
pub zone_id: String, pub zone_id: Box<str>,
pub api_key: String, pub api_key: Box<str>,
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>> {
@ -66,64 +58,9 @@ pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
Err(tried_paths) Err(tried_paths)
} }
pub async fn read_config<P: AsRef<Path>>(path: &P) -> anyhow::Result<Config> { pub async fn read_config(path: &PathBuf) -> anyhow::Result<Config> {
let mut file = fs::File::open(path).await?; let mut file = fs::File::open(&path).await?;
let mut buf = String::new(); let mut buf = String::new();
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)
}
}

View File

@ -1,43 +0,0 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tokio::{
self, signal,
sync::{futures::Notified, Notify},
};
pub struct ExitListener {
should_exit: Arc<AtomicBool>,
notify: Arc<Notify>,
}
impl ExitListener {
pub fn new() -> Self {
let this = Self {
should_exit: Arc::new(AtomicBool::new(false)),
notify: Arc::new(Notify::new()),
};
let should_exit = this.should_exit.clone();
let notify = this.notify.clone();
tokio::spawn(async move {
signal::ctrl_c()
.await
.expect("Failed to install CTRL+C signal handler");
should_exit.store(true, Ordering::SeqCst);
notify.notify_one();
});
this
}
pub fn notified(&self) -> Notified<'_> {
self.notify.notified()
}
pub fn should_exit(&self) -> bool {
self.should_exit.load(Ordering::SeqCst)
}
}

View File

@ -1,27 +0,0 @@
use std::collections::HashMap;
use serde;
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct DnsRecord {
pub id: Box<str>,
#[serde(rename = "type")]
pub record_type: Box<str>,
pub name: Box<str>,
pub content: Box<str>,
pub ttl: u32,
pub proxied: bool,
pub locked: bool,
pub zone_id: Box<str>,
pub zone_name: Box<str>,
pub modified_on: Box<str>,
pub created_on: Box<str>,
pub meta: HashMap<Box<str>, serde_json::Value>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct CloudflareResponse {
pub success: bool,
pub errors: Box<[HashMap<Box<str>, serde_json::Value>]>,
pub messages: Box<[HashMap<Box<str>, serde_json::Value>]>,
pub result: Option<Vec<DnsRecord>>,
}

View File

@ -1,3 +0,0 @@
mod cloudflare;
pub mod cloudflare_responses;
pub use cloudflare::CloudflareClient;

View File

@ -1,23 +1,15 @@
// SPDX: BSD-2-Clause mod cloudflare;
mod config; mod config;
mod exit_listener;
mod logging; mod logging;
mod message_handler;
mod network_change_listener;
mod public_ip; mod public_ip;
mod message_handler;
pub mod utils; pub mod utils;
mod tests; pub use cloudflare::CloudflareClient;
mod internet;
pub use internet::CloudflareClient;
pub use config::{get_config_path, read_config, Config}; pub use config::{get_config_path, read_config, Config};
pub use exit_listener::ExitListener;
pub use logging::init_logger; pub use logging::init_logger;
pub use message_handler::MessageHandler;
pub use network_change_listener::NetworkChangeListener;
pub use public_ip::get_current_public_ipv4; pub use public_ip::get_current_public_ipv4;
pub use message_handler::MessageHandler;
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;

View File

@ -1,5 +1,3 @@
// SPDX: BSD-2-Clause
use chrono::Local; use chrono::Local;
use colored::*; use colored::*;
use log::LevelFilter; use log::LevelFilter;

View File

@ -1,26 +1,55 @@
// SPDX: BSD-2-Clause use std::sync::{
atomic::{AtomicBool, Ordering},
use futures::future::{self, Either}; Arc,
use log::{error, info};
use scopeguard::defer;
use tokio::time;
use dynip_cloudflare::{
utils, CloudflareClient, ExitListener, MessageHandler, NetworkChangeListener,
MAX_ERORS_IN_ROW_DEFAULT,
}; };
use futures::stream::StreamExt;
use log::{error, info};
use netlink_sys::{AsyncSocket, SocketAddr};
use rtnetlink::new_connection;
use dynip_cloudflare::{utils, CloudflareClient, MessageHandler, MAX_ERORS_IN_ROW_DEFAULT};
use scopeguard::defer;
use tokio::{signal, sync::Notify};
const RTNLGRP_LINK: u32 = 1;
const RTNLGRP_IPV4_IFADDR: u32 = 5;
const fn nl_mgrp(group: u32) -> u32 {
if group > 31 {
panic!("use netlink_sys::Socket::add_membership() for this group");
}
if group == 0 {
0
} else {
1 << (group - 1)
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dynip_cloudflare::init_logger(); dynip_cloudflare::init_logger();
defer! { defer! {
log::logger().flush(); log::logger().flush();
} }
let exit_listener = ExitListener::new(); let should_exit = Arc::new(AtomicBool::new(false));
let notify = Arc::new(Notify::new());
let config = match utils::get_config().await { let should_exit_clone = should_exit.clone();
Some(aux) => aux, let notify_clone = notify.clone();
None => return,
tokio::spawn(async move {
signal::ctrl_c()
.await
.expect("Failed to install CTRL+C signal handler");
should_exit_clone.store(true, Ordering::SeqCst);
notify_clone.notify_one();
});
let config = if let Some(aux) = utils::get_config().await {
aux
} else {
return;
}; };
let mut cloudflare = let mut cloudflare =
@ -32,48 +61,29 @@ async fn main() {
} }
}; };
let mut network_change_listener = match NetworkChangeListener::new() { let (mut conn, mut _handle, mut messages) = new_connection().unwrap();
Some(aux) => { let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
info!("Listening for IPv4 address changes and interface connect/disconnect events...");
aux let addr = SocketAddr::new(0, groups);
}
None => { if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
error!("Failed to initialize networkchangelistener"); error!("Failed to bind to socket: {:?}", &e);
return; return;
} }
};
tokio::spawn(conn);
info!("Listening for IPv4 address changes and interface connect/disconnect events...");
let mut message_handler = MessageHandler::new( let mut message_handler = MessageHandler::new(
&mut cloudflare, &mut cloudflare,
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| { while !should_exit.load(Ordering::SeqCst) {
let mut interval = time::interval(duration);
interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
interval
});
while !exit_listener.should_exit() {
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! {
_ = exit_listener.notified() => break, _ = notify.notified() => break,
_ = tick_future => { message = messages.next() => {
if let Some(duration) = config.max_duration.as_ref() {
let duration_string = utils::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 = network_change_listener.next_message() => {
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;
} }
} }

View File

@ -1,28 +1,19 @@
// SPDX: BSD-2-Clause
use std::fmt; use std::fmt;
use log::{debug, error, info};
use log::{debug, error, info, log_enabled, Level};
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;
use crate::CloudflareClient; use crate::CloudflareClient;
pub struct MessageHandler<'a, Api, Zone> pub struct MessageHandler<'a> {
where cloudflare: &'a mut CloudflareClient,
Api: fmt::Display,
Zone: fmt::Display,
{
cloudflare: &'a mut CloudflareClient<Api, Zone>,
errs_counter: usize, errs_counter: usize,
errs_max: usize, errs_max: usize,
} }
impl<'a, Api, Zone> MessageHandler<'a, Api, Zone> impl<'a> MessageHandler<'a> {
where pub fn new(cloudflare: &'a mut CloudflareClient, errs_max: usize) -> Self {
Api: fmt::Display,
Zone: fmt::Display,
{
pub fn new(cloudflare: &'a mut CloudflareClient<Api, Zone>, errs_max: usize) -> Self {
Self { Self {
cloudflare, cloudflare,
errs_counter: 0, errs_counter: 0,
@ -33,14 +24,13 @@ 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(Some("New IPv4 address"), Some(&msg)) self.log_and_check("New IPv4 address", &msg).await
.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(Some("New link (interface connected)"), Some(&link)) self.log_and_check("New link (interface connected)", &link)
.await .await
} }
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => { NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
@ -48,43 +38,43 @@ where
.await .await
} }
_ => { _ => {
debug!("Unhandled message payload: {:?}", message.payload); if log_enabled!(Level::Debug) {
debug!("Unhandled message payload: {:?}", message.payload);
}
Some(()) Some(())
} }
} }
} }
pub async fn log_and_check<D, M>(&mut self, log_msg: Option<&D>, msg: Option<&M>) -> Option<()> async fn log_and_check<D, M>(&mut self, log_msg: &D, msg: &M) -> Option<()>
where where
D: fmt::Display + ?Sized, D: fmt::Display + ?Sized,
M: fmt::Debug, M: fmt::Debug,
{ {
if let Some(s) = log_msg { if log_enabled!(Level::Debug) {
info!("{}", s); debug!("{}: {:?}", log_msg, msg);
} } else {
if let Some(m) = msg { info!("{}", log_msg);
if let Some(lm) = log_msg { if let Err(e) = self.cloudflare.check().await {
debug!("{}: {:?}", lm, m); self.errs_counter += 1;
} else { error!(
debug!("{:?}", m); "Failed to check cloudflare ({}/{}): {:?}",
} self.errs_counter, self.errs_max, &e
} );
if let Err(e) = self.cloudflare.check().await { if self.errs_counter >= self.errs_max {
self.errs_counter += 1; return None;
error!( }
"Failed to check cloudflare ({}/{}): {:?}",
self.errs_counter, self.errs_max, &e
);
if self.errs_counter >= self.errs_max {
return None;
} }
} }
Some(()) Some(())
} }
async fn log_info<M: fmt::Debug>(&self, log_msg: &str, msg: &M) -> Option<()> { async fn log_info<M: fmt::Debug>(&self, log_msg: &str, msg: &M) -> Option<()> {
info!("{}", log_msg); if log_enabled!(Level::Debug) {
debug!("{:?} message: {:?}", log_msg, msg); debug!("{:?} message: {:?}", log_msg, msg);
} else {
info!("{}", log_msg);
}
Some(()) Some(())
} }
} }

View File

@ -1,60 +0,0 @@
use futures::{
channel::mpsc::UnboundedReceiver,
stream::{Next, StreamExt},
};
use log::error;
use netlink_packet_core::NetlinkMessage;
use netlink_packet_route::RouteNetlinkMessage;
use netlink_sys::{AsyncSocket, SocketAddr};
use rtnetlink::new_connection;
use tokio::task::JoinHandle;
const RTNLGRP_LINK: u32 = 1;
const RTNLGRP_IPV4_IFADDR: u32 = 5;
const fn nl_mgrp(group: u32) -> u32 {
if group > 31 {
panic!("use netlink_sys::Socket::add_membership() for this group");
}
if group == 0 {
0
} else {
1 << (group - 1)
}
}
type Messages = UnboundedReceiver<(NetlinkMessage<RouteNetlinkMessage>, SocketAddr)>;
pub struct NetworkChangeListener {
messages: Messages,
thread_handle: JoinHandle<()>,
}
impl NetworkChangeListener {
pub fn new() -> Option<Self> {
let (mut conn, mut _handle, messages) = new_connection().ok()?;
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
let addr = SocketAddr::new(0, groups);
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
error!("Failed to bind to socket: {:?}", &e);
return None;
}
let thread_handle = tokio::spawn(conn);
Some(Self {
messages,
thread_handle,
})
}
pub fn next_message(&mut self) -> Next<'_, Messages> {
self.messages.next()
}
}
impl Drop for NetworkChangeListener {
fn drop(&mut self) {
self.thread_handle.abort();
}
}

View File

@ -1,5 +1,3 @@
// SPDX: BSD-2-Clause
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use log::error; use log::error;
use reqwest::Client; use reqwest::Client;

View File

@ -1,59 +0,0 @@
use crate::Config;
use std::time::Duration;
use toml;
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());
}

View File

@ -1,2 +0,0 @@
#[cfg(test)]
mod config_serialization;

View File

@ -1,7 +1,3 @@
// 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;
@ -30,32 +26,3 @@ 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
}