This commit is contained in:
pika 2025-03-24 19:58:23 +01:00
parent 82b2885576
commit fd5840df94
5 changed files with 394 additions and 104 deletions

View file

@ -25,6 +25,7 @@ SERVER_NAME = os.getenv('SERVER_NAME', socket.gethostname())
API_KEY = os.getenv('API_KEY')
CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '60'))
VERIFY_SSL = os.getenv('VERIFY_SSL', 'true').lower() == 'true'
SERVER_TYPE = os.getenv('SERVER_TYPE', 'caddy').lower() # 'caddy' or 'nginx'
# Setup logging
logging.basicConfig(
@ -34,10 +35,11 @@ logging.basicConfig(
logger = logging.getLogger('caddy-agent')
# Debug configuration
logger.info(f"Starting Caddy agent with configuration:")
logger.info(f"Starting {SERVER_TYPE} agent with configuration:")
logger.info(f"- DASHBOARD_URL: {DASHBOARD_URL}")
logger.info(f"- SERVER_NAME: {SERVER_NAME}")
logger.info(f"- CADDYFILE_PATH: {CADDYFILE_PATH}")
logger.info(f"- SERVER_TYPE: {SERVER_TYPE}")
logger.info(f"- VERIFY_SSL: {VERIFY_SSL}")
logger.info(f"- API_KEY set: {'Yes' if API_KEY else 'No'}")
@ -101,19 +103,21 @@ def parse_caddyfile():
def create_auth_token():
"""Create a JWT token for authentication"""
if not API_KEY:
logger.error("API_KEY is not set. Authentication will fail.")
logger.error("Cannot create authentication token: API_KEY not set")
return None
# Create a token that expires in 5 minutes
# Create token with 5 minute expiry
payload = {
'server': SERVER_NAME,
'exp': datetime.now() + timedelta(minutes=5)
"exp": datetime.utcnow() + timedelta(minutes=5),
"iat": datetime.utcnow(),
"sub": SERVER_NAME
}
try:
return jwt.encode(payload, API_KEY, algorithm='HS256')
token = jwt.encode(payload, API_KEY, algorithm="HS256")
return token
except Exception as e:
logger.error(f"Error creating authentication token: {e}")
logger.error(f"Error creating JWT token: {e}")
return None
def send_update(force=False):
@ -134,7 +138,8 @@ def send_update(force=False):
data = {
"server": SERVER_NAME,
"entries": current_data,
"timestamp": current_time.isoformat()
"timestamp": current_time.isoformat(),
"type": SERVER_TYPE # Add the server type
}
# Create authentication token
@ -166,39 +171,33 @@ def send_update(force=False):
except requests.exceptions.RequestException as e:
logger.error(f"Connection error sending update: {e}")
class CaddyfileHandler(FileSystemEventHandler):
"""Watch for changes to the Caddyfile"""
class CaddyFileHandler(FileSystemEventHandler):
"""Watch for changes to the Caddyfile and send updates"""
def on_modified(self, event):
if event.src_path == CADDYFILE_PATH:
logger.info(f"Caddyfile modified: {event.src_path}")
logger.info(f"Caddyfile changed, sending update")
send_update()
def main():
"""Main function to run the agent"""
logger.info(f"Starting Caddy agent for {SERVER_NAME}")
logger.info(f"Monitoring Caddyfile at: {CADDYFILE_PATH}")
logger.info(f"Dashboard URL: {DASHBOARD_URL}")
# Initial update
"""Main function to start the agent"""
# Send initial update
send_update(force=True)
# Setup file watching
# Set up file watching
event_handler = CaddyFileHandler()
observer = Observer()
event_handler = CaddyfileHandler()
# Watch the directory containing the Caddyfile
caddyfile_dir = os.path.dirname(CADDYFILE_PATH)
observer.schedule(event_handler, caddyfile_dir, recursive=False)
observer.schedule(event_handler, path=os.path.dirname(CADDYFILE_PATH), recursive=False)
observer.start()
try:
logger.info(f"Agent started. Watching {CADDYFILE_PATH} for changes")
while True:
# Periodic check even if file doesn't change
time.sleep(CHECK_INTERVAL)
# Send periodic updates
send_update()
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":

228
app.py
View file

@ -5,7 +5,7 @@ import os
import jwt
import logging
import json
from datetime import datetime
from datetime import datetime, timedelta
from dotenv import load_dotenv
import re
import signal
@ -18,6 +18,7 @@ load_dotenv()
API_KEY = os.getenv('API_KEY')
DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true'
CADDYFILE_PATH = "/app/Caddyfile" # Fixed internal path
NGINX_CONFIG_PATH = os.getenv('NGINX_CONFIG_PATH', '/app/nginx/conf.d') # Path to nginx config directory
# Setup logging
logging.basicConfig(
@ -26,12 +27,16 @@ logging.basicConfig(
)
logger = logging.getLogger('caddy-dashboard')
# Determine if we should use local Caddyfile reading
# Determine if we should use local configuration files
USE_LOCAL_CADDYFILE = os.path.exists(CADDYFILE_PATH)
USE_LOCAL_NGINX = os.path.exists(NGINX_CONFIG_PATH) and os.path.isdir(NGINX_CONFIG_PATH)
if not USE_LOCAL_CADDYFILE:
logger.warning(f"Caddyfile not found at the standard path: {CADDYFILE_PATH}")
if not USE_LOCAL_NGINX:
logger.warning(f"Nginx config directory not found at: {NGINX_CONFIG_PATH}")
if not API_KEY:
logger.warning("API_KEY not set - running without authentication! This is insecure.")
@ -39,9 +44,9 @@ app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
# Global data storage
proxy_data = {}
deleted_servers = set()
server_last_seen = {} # Track when servers were last updated
caddy_proxies = {} # Server name -> {domain: target}
nginx_proxies = {} # Server name -> {domain: target}
timestamps = {} # Server name -> timestamp
def verify_token(token):
"""Verify the JWT token from an agent"""
@ -94,77 +99,174 @@ def parse_local_caddyfile():
return entries
def parse_nginx_configs():
"""Parse nginx config files to extract server_name and proxy_pass directives"""
entries = {}
if not os.path.exists(NGINX_CONFIG_PATH) or not os.path.isdir(NGINX_CONFIG_PATH):
logger.warning(f"Nginx config directory not found at: {NGINX_CONFIG_PATH}")
return entries
try:
# Get all .conf files in the nginx config directory
for filename in os.listdir(NGINX_CONFIG_PATH):
if not filename.endswith('.conf'):
continue
filepath = os.path.join(NGINX_CONFIG_PATH, filename)
with open(filepath, 'r') as file:
content = file.read()
# Pattern to extract server_name and proxy_pass
server_blocks = re.findall(r'server\s*{(.*?)}', content, re.DOTALL)
for block in server_blocks:
# Extract server_name
server_name_match = re.search(r'server_name\s+(.+?);', block)
if not server_name_match:
continue
server_names = server_name_match.group(1).split()
# Extract proxy_pass
proxy_pass_match = re.search(r'proxy_pass\s+(https?://[^;]+);', block)
if not proxy_pass_match:
continue
proxy_target = proxy_pass_match.group(1)
# Add each server name with its proxy target
for name in server_names:
if name != '_' and name.lower() != 'host': # Skip default server and "host"
entries[name] = proxy_target
logger.info(f"Extracted {len(entries)} domain entries from Nginx configs")
except Exception as e:
logger.error(f"Error parsing Nginx configs: {e}")
import traceback
logger.error(traceback.format_exc())
return entries
def validate_token(auth_header):
"""Validate the JWT token from the Authorization header"""
if not auth_header or not auth_header.startswith("Bearer "):
return False
token = auth_header.split(" ")[1]
try:
payload = jwt.decode(token, API_KEY, algorithms=["HS256"])
return True
except jwt.InvalidTokenError:
return False
def get_server_stats():
"""Get basic server statistics"""
stats = {
'caddy_servers': len(caddy_proxies),
'nginx_servers': len(nginx_proxies),
'total_domains': sum(len(entries) for entries in caddy_proxies.values()) +
sum(len(entries) for entries in nginx_proxies.values()),
'last_update': max(timestamps.values(), default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
}
return stats
@app.route('/')
def index():
"""Render the dashboard homepage"""
data_to_display = {k: v for k, v in proxy_data.items() if k not in deleted_servers}
# Add local Caddyfile data if available
if USE_LOCAL_CADDYFILE:
local_data = parse_local_caddyfile()
if local_data:
data_to_display["Local Server"] = local_data
# Add last seen timestamps
timestamps = {server: server_last_seen.get(server, "Never") for server in data_to_display}
return render_template('index.html', proxies=data_to_display, timestamps=timestamps)
"""Dashboard home page"""
stats = get_server_stats()
return render_template('dashboard.html',
caddy_count=stats['caddy_servers'],
nginx_count=stats['nginx_servers'],
domain_count=stats['total_domains'],
last_update=stats['last_update'])
@app.route('/caddy')
def caddy_dashboard():
"""Caddy specific dashboard"""
return render_template('index.html', proxies=caddy_proxies, timestamps=timestamps, server_type="Caddy")
@app.route('/nginx')
def nginx_dashboard():
"""Nginx specific dashboard"""
return render_template('index.html', proxies=nginx_proxies, timestamps=timestamps, server_type="Nginx")
@app.route('/api/update', methods=['POST'])
def update():
"""Secure API endpoint for agents to update their data"""
# Verify authentication
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
logger.warning("Missing or invalid Authorization header")
return jsonify({"error": "Authentication required"}), 401
token = auth_header.split(' ')[1]
payload = verify_token(token)
def update_proxies():
"""API endpoint for agents to update proxy data"""
# Verify token if API_KEY is set
if API_KEY:
auth_header = request.headers.get("Authorization")
if not validate_token(auth_header):
return jsonify({"status": "error", "message": "Invalid or missing token"}), 401
if not payload:
return jsonify({"error": "Invalid authentication token"}), 401
# Verify the server in the token matches the data
# Get data from request
data = request.json
if not data or "server" not in data or "entries" not in data:
return jsonify({"error": "Invalid data format"}), 400
if not data or not isinstance(data, dict):
return jsonify({"status": "error", "message": "Invalid data format"}), 400
server_name = data.get("server", "Local Server") # Default to 'Local Server' if not provided
entries = data.get("entries", {})
server_type = data.get("type", "caddy").lower() # Default to caddy if not specified
# Update the appropriate store based on server type
if server_type == "nginx":
nginx_proxies[server_name] = entries
else:
caddy_proxies[server_name] = entries
if payload.get('server') != data["server"]:
logger.warning(f"Server mismatch: {payload.get('server')} vs {data['server']}")
return jsonify({"error": "Server authentication mismatch"}), 403
# Update timestamp
timestamps[server_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Update the data
server_name = data["server"]
proxy_data[server_name] = data["entries"]
server_last_seen[server_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Log the update
logger.info(f"Updated {server_type} proxy data for {server_name}: {len(entries)} entries")
if server_name in deleted_servers:
deleted_servers.remove(server_name)
logger.info(f"Updated data for server: {server_name} with {len(data['entries'])} entries")
return jsonify({"message": "Updated successfully"}), 200
return jsonify({"status": "success", "message": f"Updated {len(entries)} entries for {server_name}"})
@app.route('/delete', methods=['POST'])
def delete_entry():
def delete_server():
"""Delete a server from the dashboard"""
data = request.json
server_name = data.get("server")
if not server_name or server_name not in proxy_data:
return jsonify({"error": "Server not found"}), 400
if not data or "server" not in data:
return jsonify({"status": "error", "message": "Server name required"}), 400
server_name = data["server"]
# Remove from both stores and timestamps
caddy_removed = server_name in caddy_proxies
nginx_removed = server_name in nginx_proxies
if server_name in caddy_proxies:
del caddy_proxies[server_name]
if server_name in nginx_proxies:
del nginx_proxies[server_name]
if server_name in timestamps:
del timestamps[server_name]
if not caddy_removed and not nginx_removed:
return jsonify({"status": "error", "message": f"Server {server_name} not found"}), 404
logger.info(f"Deleted server: {server_name}")
return jsonify({"status": "success", "message": f"Server {server_name} deleted"})
deleted_servers.add(server_name)
logger.info(f"Server {server_name} marked as deleted")
return jsonify({"message": f"Server {server_name} deleted"}), 200
# Initialize with local files if available
if USE_LOCAL_CADDYFILE:
entries = parse_local_caddyfile()
if entries:
caddy_proxies["Local Server"] = entries
timestamps["Local Server"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logger.info(f"Loaded {len(entries)} entries from local Caddyfile")
@app.route('/status/<domain>')
def check_status(domain):
"""Check if a subdomain is reachable"""
try:
response = requests.get(f"https://{domain}", timeout=3)
return jsonify({"status": response.status_code})
except requests.exceptions.RequestException:
return jsonify({"status": "offline"})
if USE_LOCAL_NGINX:
entries = parse_nginx_configs()
if entries:
nginx_proxies["Local Nginx"] = entries
timestamps["Local Nginx"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logger.info(f"Loaded {len(entries)} entries from local Nginx configs")
def signal_handler(sig, frame):
logger.info("Shutdown signal received, exiting gracefully...")
@ -179,8 +281,8 @@ if __name__ == '__main__':
# Load it initially
local_data = parse_local_caddyfile()
if local_data:
proxy_data["Local Server"] = local_data
server_last_seen["Local Server"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
caddy_proxies["Local Server"] = local_data
timestamps["Local Server"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if not API_KEY:
logger.warning("API_KEY not set - running without authentication!")

View file

@ -9,26 +9,49 @@ services:
- DEBUG_MODE=false
volumes:
- ./Caddyfile:/app/Caddyfile:ro
# - ./nginx:/app/nginx:ro # Mount nginx config directory
command: server
restart: unless-stopped
networks:
- caddy-network
# networks:
# - caddy-network
# Agent mode (example)
caddydb-agent:
# Caddy Agent mode
caddydb-caddy-agent:
image: caddydb:latest
volumes:
- /path/to/host/Caddyfile:/app/Caddyfile:ro
- /path/to/your/Caddyfile:/app/Caddyfile:ro
environment:
- API_KEY=${API_KEY}
- DASHBOARD_URL=http://caddydb-server:5000/api/update
- SERVER_NAME=caddy-server-1
- SERVER_TYPE=caddy
- CHECK_INTERVAL=60
- VERIFY_SSL=false # Set to false if using self-signed certificates
- VERIFY_SSL=false
command: agent
restart: unless-stopped
networks:
- caddy-network
# networks:
# - caddy-network
# depends_on:
# - caddydb-server
# Nginx Agent mode (example)
caddydb-nginx-agent:
image: caddydb:latest
volumes:
- /path/to/your/nginx/conf.d:/app/nginx:ro
environment:
- API_KEY=${API_KEY}
- DASHBOARD_URL=http://caddydb-server:5000/api/update
- SERVER_NAME=nginx-server-1
- SERVER_TYPE=nginx
- CHECK_INTERVAL=60
- VERIFY_SSL=false
command: agent
restart: unless-stopped
# networks:
# - caddy-network
# depends_on:
# - caddydb-server
networks:
caddy-network:

149
templates/dashboard.html Normal file
View file

@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Caddy Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
background-color: #0f0f0f;
background-image:
radial-gradient(circle at 25% 25%, rgba(40, 40, 40, 0.05) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(40, 40, 40, 0.05) 0%, transparent 50%);
}
.bg-grid {
background-size: 50px 50px;
background-image:
linear-gradient(to right, rgba(40, 40, 40, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(40, 40, 40, 0.05) 1px, transparent 1px);
}
.card {
transition: all 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body class="text-gray-200 min-h-screen bg-grid">
<!-- Minimal header -->
<header class="py-4 px-6 bg-black/50 backdrop-blur-sm border-b border-gray-800/50">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-xl font-medium tracking-tight text-white">Caddy Dashboard</h1>
<div class="flex items-center gap-3">
<a href="/caddy"
class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md transition-colors">
<i class="fas fa-server text-xs mr-1.5"></i>Caddy
</a>
<a href="/nginx"
class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md transition-colors">
<i class="fas fa-server text-xs mr-1.5"></i>Nginx
</a>
</div>
</div>
</header>
<div class="container mx-auto p-4 md:p-6">
<!-- Overview Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
<div class="flex items-center">
<div class="w-12 h-12 flex items-center justify-center bg-blue-500/10 text-blue-400 rounded-lg mr-4">
<i class="fas fa-server text-xl"></i>
</div>
<div>
<p class="text-sm text-gray-500">Caddy Servers</p>
<h3 class="text-xl font-medium text-white">{{ caddy_count }}</h3>
</div>
</div>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
<div class="flex items-center">
<div class="w-12 h-12 flex items-center justify-center bg-green-500/10 text-green-400 rounded-lg mr-4">
<i class="fas fa-server text-xl"></i>
</div>
<div>
<p class="text-sm text-gray-500">Nginx Servers</p>
<h3 class="text-xl font-medium text-white">{{ nginx_count }}</h3>
</div>
</div>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
<div class="flex items-center">
<div class="w-12 h-12 flex items-center justify-center bg-purple-500/10 text-purple-400 rounded-lg mr-4">
<i class="fas fa-globe text-xl"></i>
</div>
<div>
<p class="text-sm text-gray-500">Total Domains</p>
<h3 class="text-xl font-medium text-white">{{ domain_count }}</h3>
</div>
</div>
</div>
<div class="card bg-zinc-900 border border-zinc-800 rounded-lg p-5">
<div class="flex items-center">
<div class="w-12 h-12 flex items-center justify-center bg-amber-500/10 text-amber-400 rounded-lg mr-4">
<i class="fas fa-clock text-xl"></i>
</div>
<div>
<p class="text-sm text-gray-500">Last Update</p>
<h3 class="text-sm font-medium text-white">{{ last_update }}</h3>
</div>
</div>
</div>
</div>
<!-- Server Selection Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<a href="/caddy" class="card bg-zinc-900 border border-zinc-800 rounded-lg p-6 hover:border-blue-500/50">
<div class="flex items-center mb-4">
<div class="w-14 h-14 flex items-center justify-center bg-blue-500/10 text-blue-400 rounded-lg mr-5">
<i class="fas fa-server text-2xl"></i>
</div>
<div>
<h3 class="text-xl font-medium text-white">Caddy Server Dashboard</h3>
<p class="text-sm text-gray-500 mt-1">View all Caddy reverse proxy configurations</p>
</div>
</div>
<div class="flex justify-between items-center mt-2">
<span class="text-xs text-gray-500">{{ caddy_count }} servers</span>
<span class="text-blue-400 text-sm">View <i class="fas fa-arrow-right ml-1"></i></span>
</div>
</a>
<a href="/nginx" class="card bg-zinc-900 border border-zinc-800 rounded-lg p-6 hover:border-green-500/50">
<div class="flex items-center mb-4">
<div class="w-14 h-14 flex items-center justify-center bg-green-500/10 text-green-400 rounded-lg mr-5">
<i class="fas fa-server text-2xl"></i>
</div>
<div>
<h3 class="text-xl font-medium text-white">Nginx Server Dashboard</h3>
<p class="text-sm text-gray-500 mt-1">View all Nginx proxy configurations</p>
</div>
</div>
<div class="flex justify-between items-center mt-2">
<span class="text-xs text-gray-500">{{ nginx_count }} servers</span>
<span class="text-green-400 text-sm">View <i class="fas fa-arrow-right ml-1"></i></span>
</div>
</a>
</div>
</div>
<footer class="py-4 mt-12 border-t border-zinc-900">
<div class="container mx-auto px-4 text-center text-xs text-gray-600">
Caddy Dashboard • Supports both Caddy and Nginx configurations
</div>
</footer>
</body>
</html>

View file

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Caddy Dashboard</title>
<title>{{ server_type }} Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@ -98,12 +98,29 @@
<!-- Minimal header -->
<header class="py-4 px-6 bg-black/50 backdrop-blur-sm border-b border-gray-800/50">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-xl font-medium tracking-tight text-white">Caddy Dashboard</h1>
<button onclick="toggleSearch()" class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md
transition-colors flex items-center gap-2">
<i class="fas fa-search text-xs"></i>
<span class="hidden sm:inline">/</span>
</button>
<div class="flex items-center">
<a href="/" class="text-gray-400 hover:text-white mr-3">
<i class="fas fa-home"></i>
</a>
<h1 class="text-xl font-medium tracking-tight text-white">
{{ server_type }} Dashboard
</h1>
</div>
<div class="flex items-center gap-3">
<a href="/caddy"
class="text-sm px-3 py-1.5 {% if server_type == 'Caddy' %}bg-blue-900/50 border-blue-700/50{% else %}bg-zinc-800 border-zinc-700{% endif %} hover:bg-zinc-700 border rounded-md transition-colors">
<i class="fas fa-server text-xs mr-1.5"></i>Caddy
</a>
<a href="/nginx"
class="text-sm px-3 py-1.5 {% if server_type == 'Nginx' %}bg-green-900/50 border-green-700/50{% else %}bg-zinc-800 border-zinc-700{% endif %} hover:bg-zinc-700 border rounded-md transition-colors">
<i class="fas fa-server text-xs mr-1.5"></i>Nginx
</a>
<button onclick="toggleSearch()" class="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-md
transition-colors flex items-center gap-2">
<i class="fas fa-search text-xs"></i>
<span class="hidden sm:inline">/</span>
</button>
</div>
</div>
</header>