enhancement

This commit is contained in:
Francesco Picone
2025-12-29 12:55:53 +01:00
parent 74bdffc411
commit 7c00b1af87
7 changed files with 260 additions and 24 deletions

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Environment
.env
.env.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
# Docker
data/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log

114
README.md Normal file
View File

@@ -0,0 +1,114 @@
# Cloudflare DDNS Updater con UI
Sistema automatico per aggiornare i record DNS di Cloudflare con il tuo IP pubblico dinamico, completo di interfaccia web per monitoraggio.
## Caratteristiche
- ✅ Aggiornamento automatico DNS tramite container `oznu/cloudflare-ddns`
- ✅ Interfaccia web Flask per monitoraggio in tempo reale
- ✅ Auto-refresh AJAX senza ricaricare la pagina
- ✅ Visualizzazione stato di sincronizzazione IP
- ✅ Cache intelligente per ridurre chiamate API
- ✅ Health check e logging strutturato
- ✅ Design responsive dark mode
## Requisiti
- Docker e Docker Compose
- Token API Cloudflare con permessi DNS edit
- Zona DNS configurata su Cloudflare
## Configurazione
1. Crea il file `.env` nella root del progetto:
```env
# Cloudflare
CF_API_TOKEN=your_cloudflare_api_token
CF_ZONE=tuodomain.it
CF_SUBDOMAIN=subdomain
CF_PROXIED=false
CF_TTL=120
# Frequenza controllo IP (cron syntax)
CF_CRON=*/5 * * * *
# Configurazione Container
DATA_VOLUME=./data
RESTART_POLICY=unless-stopped
# UI Settings
UI_REFRESH_SECONDS=60
UI_PORT=8088
```
2. Avvia i container:
```bash
docker-compose up -d
```
3. Accedi all'interfaccia web:
```
http://localhost:8088
```
## Struttura Progetto
```
.
├── docker-compose.yml # Configurazione Docker
├── .env # Variabili ambiente (gitignored)
├── ui/
│ ├── app.py # Server Flask
│ ├── cloudflare.py # API Cloudflare
│ ├── requirements.txt # Dipendenze Python
│ └── templates/
│ └── index.html # UI web
└── data/ # Dati persistenti (gitignored)
```
## API Endpoints
- `GET /` - Interfaccia web
- `GET /api/records` - JSON con stato record DNS
- `GET /health` - Health check
## Monitoraggio
L'interfaccia mostra per ogni record:
- Nome FQDN
- IP configurato nel DNS
- IP pubblico attuale
- Stato Proxy Cloudflare
- Stato sincronizzazione (OK/MISMATCH/ERROR)
- Ultimo aggiornamento
## Ottimizzazioni
- ✅ Cache Zone ID per evitare chiamate ripetute
- ✅ Timeout su tutte le richieste HTTP
- ✅ Validazione configurazione all'avvio
- ✅ CORS abilitato per integrazioni esterne
- ✅ Auto-refresh AJAX senza refresh completo
- ✅ Gestione errori e logging strutturato
## Troubleshooting
Verifica i log dei container:
```bash
docker-compose logs cloudflare-ddns
docker-compose logs cloudflare-ddns-ui
```
Verifica l'health status:
```bash
curl http://localhost:8088/health
```
## Licenza
MIT

View File

@@ -30,7 +30,14 @@ services:
- ./ui:/app
working_dir: /app
command: >
sh -c "pip install --no-cache-dir flask requests &&
sh -c "pip install --no-cache-dir -r requirements.txt &&
python app.py"
ports:
- "${UI_PORT}:5000"
healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
interval: 30s
timeout: 10s
retries: 3
depends_on:
- cloudflare-ddns

View File

@@ -1,25 +1,61 @@
from flask import Flask, render_template_string
from flask import Flask, render_template_string, jsonify
from flask_cors import CORS
from cloudflare import get_dns_records
import os
import logging
# Configurazione logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app) # Abilita CORS per richieste AJAX
# ----------------------------
# Template HTML già esistente
# Template HTML
# ----------------------------
TEMPLATE = open("templates/index.html").read() # Se preferisci usare file separato
with open("templates/index.html", encoding="utf-8") as f:
TEMPLATE = f.read()
REFRESH_SECONDS = int(os.getenv("REFRESH_SECONDS", "60"))
# ----------------------------
# Endpoint principale
# ----------------------------
@app.route("/")
def index():
try:
records = get_dns_records()
return render_template_string(TEMPLATE, records=records, refresh_seconds=REFRESH_SECONDS)
except Exception as e:
logger.error(f"Errore nel caricamento dei record: {e}")
return render_template_string(TEMPLATE, records=[], refresh_seconds=REFRESH_SECONDS)
# Passiamo i dati al template
return render_template_string(TEMPLATE, records=records)
# ----------------------------
# API endpoint per refresh AJAX
# ----------------------------
@app.route("/api/records")
def api_records():
try:
records = get_dns_records()
return jsonify({"success": True, "records": records})
except Exception as e:
logger.error(f"Errore API: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ----------------------------
# Health check
# ----------------------------
@app.route("/health")
def health():
return jsonify({"status": "ok"}), 200
# ----------------------------
# Avvio Flask
# ----------------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
logger.info(f"Avvio server su porta 5000 con refresh ogni {REFRESH_SECONDS}s")
app.run(host="0.0.0.0", port=5000, debug=False)

View File

@@ -5,10 +5,14 @@ from datetime import datetime
# ----------------------------
# Config da .env
# ----------------------------
CF_API_TOKEN = os.getenv("CF_UI_API_TOKEN")
CF_API_TOKEN = os.getenv("CF_API_TOKEN")
CF_ZONE = os.getenv("CF_ZONE")
CF_SUBDOMAINS = os.getenv("CF_UI_SUBDOMAINS", "").split(",")
PUBLIC_IP_API = os.getenv("PUBLIC_IP_API", "https://api.ipify.org") # opzionale
CF_SUBDOMAINS = [s.strip() for s in os.getenv("CF_SUBDOMAINS", "").split(",") if s.strip()]
PUBLIC_IP_API = os.getenv("PUBLIC_IP_API", "https://api.ipify.org")
# Validazione configurazione
if not CF_API_TOKEN or not CF_ZONE or not CF_SUBDOMAINS:
raise ValueError("Mancano variabili ambiente: CF_API_TOKEN, CF_ZONE, CF_SUBDOMAINS")
HEADERS = {
"Authorization": f"Bearer {CF_API_TOKEN}",
@@ -26,16 +30,24 @@ def get_public_ip():
except Exception:
return "N/A"
_zone_id_cache = None
def get_zone_id():
"""Recupera l'ID della zona Cloudflare"""
"""Recupera l'ID della zona Cloudflare (con cache)"""
global _zone_id_cache
if _zone_id_cache:
return _zone_id_cache
try:
resp = requests.get(
"https://api.cloudflare.com/client/v4/zones",
headers=HEADERS,
params={"name": CF_ZONE}
params={"name": CF_ZONE},
timeout=10
).json()
if resp["success"] and resp["result"]:
return resp["result"][0]["id"]
_zone_id_cache = resp["result"][0]["id"]
return _zone_id_cache
else:
raise Exception(f"Zona '{CF_ZONE}' non trovata o errore API.")
except Exception as e:
@@ -57,40 +69,47 @@ def get_dns_records():
resp = requests.get(
f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records",
headers=HEADERS,
params={"name": fqdn}
params={"name": fqdn},
timeout=10
).json()
if not resp["success"] or not resp["result"]:
# Record non trovato
now = datetime.utcnow().isoformat()
records.append({
"name": fqdn,
"dns_ip": "N/A",
"public_ip": public_ip,
"proxied": False,
"status": "MISSING",
"last_updated": datetime.utcnow().isoformat()
"last_updated": now,
"last_updated_human": now
})
continue
dns = resp["result"][0]
last_updated = dns.get("modified_on", datetime.utcnow().isoformat())
records.append({
"name": fqdn,
"dns_ip": dns["content"],
"public_ip": public_ip,
"proxied": dns["proxied"],
"status": "OK" if dns["content"] == public_ip else "MISMATCH",
"last_updated": dns.get("modified_on", datetime.utcnow().isoformat())
"last_updated": last_updated,
"last_updated_human": last_updated
})
except Exception as e:
print(f"[ERROR] get_dns_records ({fqdn}): {e}")
now = datetime.utcnow().isoformat()
records.append({
"name": fqdn,
"dns_ip": "ERROR",
"public_ip": public_ip,
"proxied": False,
"status": "ERROR",
"last_updated": datetime.utcnow().isoformat()
"last_updated": now,
"last_updated_human": now
})
return records

3
ui/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
flask==3.1.0
flask-cors==5.0.0
requests==2.32.3

View File

@@ -112,8 +112,8 @@
</table>
</main>
<footer>
Visualizzazione aggiornata ogni 60 secondi
<footer id="status">
Visualizzazione aggiornata ogni {{ refresh_seconds }} secondi | Ultimo aggiornamento: <span id="last-update">ora</span>
</footer>
<script>
@@ -128,9 +128,41 @@ function timeAgo(date) {
return "pochi secondi fa";
}
function updateTimeAgo() {
document.querySelectorAll('.time').forEach(td => {
td.textContent = timeAgo(td.dataset.timestamp);
});
}
function updateRecords() {
fetch('/api/records')
.then(res => res.json())
.then(data => {
if (data.success) {
const tbody = document.querySelector('tbody');
tbody.innerHTML = data.records.map(r => `
<tr>
<td>${r.name}</td>
<td>${r.dns_ip}</td>
<td>${r.public_ip}</td>
<td>${r.proxied ? 'ON' : 'OFF'}</td>
<td><span class="badge ${r.status === 'OK' ? 'ok' : 'bad'}">${r.status}</span></td>
<td class="time" data-timestamp="${r.last_updated}">${timeAgo(r.last_updated)}</td>
</tr>
`).join('');
document.getElementById('last-update').textContent = new Date().toLocaleTimeString('it-IT');
}
})
.catch(err => {
console.error('Errore aggiornamento:', err);
document.getElementById('status').style.color = '#ff5252';
});
}
// Init
updateTimeAgo();
setInterval(updateTimeAgo, 5000);
setInterval(updateRecords, {{ refresh_seconds }} * 1000);
</script>
</body>