#!/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 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 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[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?Phttps?:\/\/[\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 send_update(force=False): """Send Caddyfile data to the dashboard server""" global last_data_sent, last_send_time # Parse the Caddyfile 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 # Always include the server name in data payload data = { "server": SERVER_NAME, # Always include this "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 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 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 = CaddyFileHandler() observer = Observer() 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: # Send periodic updates send_update() time.sleep(CHECK_INTERVAL) except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": main()