caddydb/agent.py
2025-03-30 14:05:09 +02:00

286 lines
No EOL
9.7 KiB
Python

#!/usr/bin/env python3
import os
import time
import json
import jwt
import requests
import socket
import re
import logging
import sys
from datetime import datetime, timedelta
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from dotenv import load_dotenv
import urllib3
# Load environment variables
load_dotenv()
# Fixed configuration
CADDYFILE_PATH = "/app/Caddyfile" # Fixed internal path
NGINX_CONFIG_PATH = "/app/nginx" # Fixed internal path for nginx configs
DASHBOARD_URL = os.getenv('DASHBOARD_URL', 'http://caddydb-server:5000/api/update')
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(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('caddy-agent')
# Debug 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'}")
# Disable SSL warnings if verification is disabled
if not VERIFY_SSL:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger.warning("SSL verification is disabled - this is insecure!")
# Validate required configuration
if not os.path.exists(CADDYFILE_PATH):
logger.error(f"Caddyfile not found at {CADDYFILE_PATH}")
sys.exit(1)
if not DASHBOARD_URL:
logger.error("DASHBOARD_URL environment variable not set - cannot send updates")
sys.exit(1)
if not API_KEY:
logger.warning("API_KEY environment variable not set - authentication will fail!")
# Last data sent to avoid unnecessary updates
last_data_sent = None
last_send_time = datetime.min
# Flag to determine what type of config to monitor
IS_NGINX = SERVER_TYPE.lower() == 'nginx'
def parse_caddyfile():
"""Parse the Caddyfile to extract domains and their proxy targets"""
entries = {}
try:
if not os.path.exists(CADDYFILE_PATH):
logger.error(f"Caddyfile not found at {CADDYFILE_PATH}")
return entries
with open(CADDYFILE_PATH, "r") as file:
content = file.read()
# Revert to simpler pattern that was working previously
pattern = re.compile(r"(?P<domains>[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?P<target>https?:\/\/[\d\.]+:\d+|[\d\.]+:\d+).*?}", re.DOTALL)
matches = pattern.findall(content)
logger.info(f"Found {len(matches)} matches in Caddyfile")
for domains, target in matches:
for domain in re.split(r',\s*', domains):
domain = domain.strip()
if domain and domain.lower() != "host": # Skip entries named "host"
entries[domain] = target.strip()
logger.info(f"Extracted {len(entries)} domain entries from Caddyfile")
# Debug output of parsed entries
for domain, target in entries.items():
logger.debug(f"Domain: {domain} -> {target}")
except Exception as e:
logger.error(f"Error parsing Caddyfile: {e}")
import traceback
logger.error(traceback.format_exc())
return entries
def create_auth_token():
"""Create a JWT token for authentication"""
if not API_KEY:
logger.error("Cannot create authentication token: API_KEY not set")
return None
# Create token with 5 minute expiry
payload = {
"exp": datetime.utcnow() + timedelta(minutes=5),
"iat": datetime.utcnow(),
"sub": SERVER_NAME
}
try:
token = jwt.encode(payload, API_KEY, algorithm="HS256")
return token
except Exception as e:
logger.error(f"Error creating JWT token: {e}")
return None
def parse_nginx_configs():
"""Parse Nginx config files to extract domains and their proxy targets"""
entries = {}
if not os.path.exists(NGINX_CONFIG_PATH) or not os.path.isdir(NGINX_CONFIG_PATH):
logger.error(f"Nginx config directory not found at {NGINX_CONFIG_PATH}")
return entries
try:
# Find all .conf files in the directory and subdirectories
conf_files = []
for root, _, files in os.walk(NGINX_CONFIG_PATH):
for file in files:
if file.endswith('.conf'):
conf_files.append(os.path.join(root, file))
logger.info(f"Found {len(conf_files)} Nginx config files")
# Pattern to match server_name and proxy_pass directives
server_name_pattern = re.compile(r'server_name\s+([^;]+);', re.IGNORECASE)
proxy_pass_pattern = re.compile(r'proxy_pass\s+([^;]+);', re.IGNORECASE)
for conf_file in conf_files:
try:
with open(conf_file, 'r') as file:
content = file.read()
# Extract server blocks
server_blocks = re.findall(r'server\s*{([^}]+)}', content, re.DOTALL)
for block in server_blocks:
server_names = server_name_pattern.search(block)
proxy_pass = proxy_pass_pattern.search(block)
if server_names and proxy_pass:
server_names = server_names.group(1).strip().split()
target = proxy_pass.group(1).strip()
for name in server_names:
# Skip default names like "_" or localhost
if name != "_" and name != "localhost" and name != "localhost.localdomain":
entries[name] = target
except Exception as e:
logger.error(f"Error parsing Nginx config file {conf_file}: {e}")
return entries
except Exception as e:
logger.error(f"Error parsing Nginx configs: {e}")
return entries
def send_update(force=False):
"""Send configuration data to the dashboard server"""
global last_data_sent, last_send_time
# Parse the appropriate configuration
if IS_NGINX:
current_data = parse_nginx_configs()
else:
current_data = parse_caddyfile()
current_time = datetime.now()
# Only send if data changed or enough time passed since last update
if (not force and
current_data == last_data_sent and
(current_time - last_send_time).total_seconds() < CHECK_INTERVAL):
return
# Create the data payload
data = {
"server": SERVER_NAME,
"entries": current_data,
"timestamp": current_time.isoformat(),
"type": SERVER_TYPE
}
# Create authentication token
token = create_auth_token()
if not token:
return
# Prepare headers with authentication
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
}
try:
response = requests.post(
DASHBOARD_URL,
json=data,
headers=headers,
timeout=10,
verify=VERIFY_SSL
)
if response.status_code == 200:
logger.info(f"Update sent successfully: {response.json()}")
last_data_sent = current_data
last_send_time = current_time
else:
logger.error(f"Error sending update: {response.status_code} - {response.text}")
except requests.exceptions.RequestException as e:
logger.error(f"Connection error sending update: {e}")
class ConfigFileHandler(FileSystemEventHandler):
"""Watch for changes to configuration files and send updates"""
def on_modified(self, event):
if IS_NGINX:
# For Nginx, check if it's a .conf file in the watched directory
if event.src_path.endswith('.conf'):
logger.info(f"Nginx config changed: {event.src_path}, sending update")
send_update()
else:
# For Caddy, check if it's the Caddyfile
if event.src_path == CADDYFILE_PATH:
logger.info(f"Caddyfile changed, sending update")
send_update()
def main():
"""Main function to start the agent"""
# Send initial update
send_update(force=True)
# Set up file watching
event_handler = ConfigFileHandler()
observer = Observer()
if IS_NGINX:
# Watch the Nginx config directory
if not os.path.exists(NGINX_CONFIG_PATH):
logger.error(f"Nginx config path not found: {NGINX_CONFIG_PATH}")
sys.exit(1)
observer.schedule(event_handler, path=NGINX_CONFIG_PATH, recursive=True)
logger.info(f"Watching Nginx configs in {NGINX_CONFIG_PATH} for changes")
else:
# Watch the Caddyfile
if not os.path.exists(CADDYFILE_PATH):
logger.error(f"Caddyfile not found: {CADDYFILE_PATH}")
sys.exit(1)
observer.schedule(event_handler, path=os.path.dirname(CADDYFILE_PATH), recursive=False)
logger.info(f"Watching {CADDYFILE_PATH} for changes")
observer.start()
try:
logger.info(f"{SERVER_TYPE.capitalize()} agent started successfully")
while True:
# Send periodic updates
send_update()
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
main()