feat: Add smart JSON storage and intelligent config checking v1.2.0

- Add automatic JSON file creation when none exists
- Implement IP presence checking before config updates
- Only update existing entries, never add new ones automatically
- Enhanced Apache support with proper directive detection
- Improved debug logging and error handling
- Non-intrusive behavior preserves existing configurations
This commit is contained in:
koenieee
2025-10-03 11:35:10 +02:00
parent ac269dcb0a
commit c530c5ae13
29 changed files with 119 additions and 404 deletions
+24
View File
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2025-10-03
### Added
- **Smart JSON Storage**: Automatic creation of IP tracking files when none exist
- Creates JSON files with hostname, IP, and timestamps automatically
- Prevents duplicate work by storing resolved IP addresses
- **Intelligent Config Checking**: Pre-update verification of IP presence in config files
- Checks if current IP already exists in nginx/apache configurations
- Avoids unnecessary updates when IP is already correctly configured
- **Non-Intrusive Behavior**: Enhanced update logic for better reliability
- Only updates existing allow/deny entries, never adds new ones automatically
- Provides clear feedback when no changes are needed
- **Enhanced Apache Support**: Extended IP checking for Apache configurations
- Supports both "Allow from" and "Require ip" directive formats
- Consistent behavior between nginx and Apache handlers
### Changed
- **Update Logic**: Improved decision-making process for config updates
- Better handling of first-run scenarios without existing JSON files
- More granular control over when configuration changes occur
- **Debug Logging**: Enhanced debugging output for troubleshooting
- Clearer messages about JSON file creation and IP checking
- Better visibility into decision-making process
## [1.1.9] - 2025-01-03
### Fixed
Generated
+1 -1
View File
@@ -291,7 +291,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "ddns_updater"
version = "1.1.9"
version = "1.2.0"
dependencies = [
"async-trait",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ddns_updater"
version = "1.1.9"
version = "1.2.0"
edition = "2021" # Use 2021 edition for better compatibility
default-run = "ddns_updater"
rust-version = "1.82" # Specify minimum Rust version for Debian 12
+3
View File
@@ -10,6 +10,9 @@ A Rust-based Dynamic DNS (DDNS) updater that automatically manages nginx allow l
## Features
- **Automatic IP Detection**: Monitors your public IP address changes
- **Smart JSON Storage**: Automatically creates and manages IP tracking files
- **Intelligent Config Checking**: Verifies IP presence before making changes
- **Non-Intrusive Updates**: Only updates existing entries, never adds new ones
- **Multiple Config Support**: Process single files or entire directories
- **Smart Cleanup**: Removes old duplicate IP entries for the same host
- **Nginx Integration**: Automatically validates and reloads nginx configurations
+6
View File
@@ -55,6 +55,12 @@ pub trait WebServerHandler: Send + Sync {
config: &WebServerConfig,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>>;
async fn check_ip_in_config(
&self,
config: &WebServerConfig,
ip: IpAddr,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>>;
fn server_type(&self) -> crate::domain::entities::WebServerType;
}
+33 -4
View File
@@ -70,11 +70,40 @@ impl DdnsUpdateService {
let stored_ip = self.ip_repository.load_ip(hostname).await?;
eprintln!("DEBUG: Stored IP: {:?}", stored_ip);
// Check if IP has changed
if let Some(old_ip) = stored_ip {
if old_ip == current_ip {
eprintln!("DEBUG: IP unchanged, returning NoChange");
// If no JSON file exists, create it with current IP and check config
if stored_ip.is_none() {
eprintln!("DEBUG: No stored IP found, creating JSON file and checking config");
// Store the current IP in JSON file
self.ip_repository.store_ip(hostname, current_ip).await?;
eprintln!("DEBUG: Created new JSON file with IP: {}", current_ip);
// Check if the current IP is already in the config file
let ip_in_config = self
.web_server_handler
.check_ip_in_config(config, current_ip)
.await?;
if ip_in_config {
eprintln!(
"DEBUG: IP {} already exists in config, no update needed",
current_ip
);
return Ok(UpdateResult::NoChange { ip: current_ip });
} else {
eprintln!(
"DEBUG: IP {} not found in config, but not adding new entries",
current_ip
);
return Ok(UpdateResult::NoChange { ip: current_ip });
}
} else {
// Check if IP has changed
if let Some(old_ip) = stored_ip {
if old_ip == current_ip {
eprintln!("DEBUG: IP unchanged, returning NoChange");
return Ok(UpdateResult::NoChange { ip: current_ip });
}
}
}
+27
View File
@@ -166,6 +166,33 @@ impl WebServerHandler for ApacheHandler {
self.validate_config(config).await
}
async fn check_ip_in_config(
&self,
config: &WebServerConfig,
ip: IpAddr,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let content = fs::read_to_string(&config.path).await?;
let ip_str = ip.to_string();
// Check if the IP exists in any Allow directive (Apache format)
for line in content.lines() {
let trimmed = line.trim();
// Apache uses "Allow from" or "Require ip" directives
if (trimmed.starts_with("Allow from ") || trimmed.starts_with("Require ip "))
&& trimmed.contains(&ip_str)
{
eprintln!(
"DEBUG: Found IP {} in Apache config line: {}",
ip_str, trimmed
);
return Ok(true);
}
}
eprintln!("DEBUG: IP {} not found in Apache config file", ip_str);
Ok(false)
}
fn server_type(&self) -> WebServerType {
WebServerType::Apache
}
+24 -6
View File
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use std::process::Command;
use tokio::fs;
use crate::domain::entities::{WebServerConfig, WebServerType};
use crate::domain::entities::WebServerConfig;
use crate::domain::ports::WebServerHandler;
/// Nginx web server handler
@@ -117,9 +117,6 @@ impl NginxHandler {
}
}
// Important: Do NOT add new entries if none existed before
// This ensures we only update existing entries
if updated {
let new_content = lines.join("\n");
eprintln!("DEBUG: About to write to config file: {:?}", config_path);
@@ -242,8 +239,29 @@ impl WebServerHandler for NginxHandler {
self.validate_config(config).await
}
fn server_type(&self) -> WebServerType {
WebServerType::Nginx
async fn check_ip_in_config(
&self,
config: &WebServerConfig,
ip: IpAddr,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let content = fs::read_to_string(&config.path).await?;
let ip_str = ip.to_string();
// Check if the IP exists in any allow directive
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("allow ") && trimmed.contains(&ip_str) {
eprintln!("DEBUG: Found IP {} in config line: {}", ip_str, trimmed);
return Ok(true);
}
}
eprintln!("DEBUG: IP {} not found in config file", ip_str);
Ok(false)
}
fn server_type(&self) -> crate::domain::entities::WebServerType {
crate::domain::entities::WebServerType::Nginx
}
}
@@ -1,12 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.1;
allow 142.250.102.138;
deny all;
}
}
@@ -1,12 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.1;
allow 142.250.102.138;
deny all;
}
}
@@ -1,12 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.1;
allow 142.250.102.138;
deny all;
}
}
@@ -1,12 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.1;
allow 142.250.102.138;
deny all;
}
}
@@ -1,12 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.1;
allow 142.250.102.138;
deny all;
}
}
@@ -1,27 +0,0 @@
upstream backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
allow 10.0.0.0/8;
allow 142.250.102.101;
deny all;
}
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
@@ -1,27 +0,0 @@
upstream backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
allow 10.0.0.0/8;
allow 142.250.102.101;
deny all;
}
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
@@ -1,27 +0,0 @@
upstream backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
allow 10.0.0.0/8;
allow 142.250.102.101;
deny all;
}
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
@@ -1,27 +0,0 @@
upstream backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
allow 10.0.0.0/8;
allow 142.250.102.101;
deny all;
}
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
@@ -1,35 +0,0 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/plain application/json;
server {
listen 80;
server_name www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
location / {
try_files $uri $uri/ /index.php?$query_string;
allow 203.0.113.0/24;
allow 142.250.102.139;
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
}
@@ -1,35 +0,0 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/plain application/json;
server {
listen 80;
server_name www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
location / {
try_files $uri $uri/ /index.php?$query_string;
allow 203.0.113.0/24;
allow 142.250.102.139;
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
}
@@ -1,35 +0,0 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/plain application/json;
server {
listen 80;
server_name www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
location / {
try_files $uri $uri/ /index.php?$query_string;
allow 203.0.113.0/24;
allow 142.250.102.139;
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
}
@@ -1,35 +0,0 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/plain application/json;
server {
listen 80;
server_name www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
location / {
try_files $uri $uri/ /index.php?$query_string;
allow 203.0.113.0/24;
allow 142.250.102.139;
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
}
@@ -1,10 +0,0 @@
server {
allow 23.192.228.80;
allow 142.250.102.113;
listen 80;
server_name minimal.example.com;
location / {
return 200 "Hello World";
}
}
@@ -1,10 +0,0 @@
server {
allow 23.192.228.80;
allow 142.250.102.113;
listen 80;
server_name minimal.example.com;
location / {
return 200 "Hello World";
}
}
@@ -1,10 +0,0 @@
server {
allow 23.192.228.80;
allow 142.250.102.113;
listen 80;
server_name minimal.example.com;
location / {
return 200 "Hello World";
}
}
@@ -1,10 +0,0 @@
server {
allow 23.192.228.80;
allow 142.250.102.113;
listen 80;
server_name minimal.example.com;
location / {
return 200 "Hello World";
}
}
@@ -1,11 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.0/24;
deny all;
}
}
@@ -1,11 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.0/24;
deny all;
}
}
@@ -1,11 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.0/24;
deny all;
}
}
@@ -1,11 +0,0 @@
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
allow 192.168.1.0/24;
deny all;
}
}