#!/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 # Load environment variables load_dotenv() # Fixed configuration CADDYFILE_PATH = "/app/Caddyfile" # Fixed internal path DASHBOARD_URL = os.getenv('DASHBOARD_URL') SERVER_NAME = os.getenv('SERVER_NAME', socket.gethostname()) API_KEY = os.getenv('API_KEY') CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '60')) # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('caddy-agent') # 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() pattern = re.compile(r"(?P[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?Phttps?:\/\/[\d\.]+:\d+|[\d\.]+:\d+).*?}", re.DOTALL) matches = pattern.findall(content) for domains, target in matches: for domain in domains.split(", "): domain = domain.strip() if domain: # Only add non-empty domains entries[domain] = target.strip() logger.info(f"Found {len(entries)} domain entries in Caddyfile") except Exception as e: logger.error(f"Error parsing Caddyfile: {e}") return entries def create_auth_token(): """Create a JWT token for authentication""" if not API_KEY: logger.error("API_KEY is not set. Authentication will fail.") return None # Create a token that expires in 5 minutes payload = { 'server': SERVER_NAME, 'exp': datetime.utcnow() + timedelta(minutes=5) } try: return jwt.encode(payload, API_KEY, algorithm='HS256') except Exception as e: logger.error(f"Error creating authentication 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 # Create the data payload data = { "server": SERVER_NAME, "entries": current_data, "timestamp": current_time.isoformat() } # 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 # Set a reasonable timeout ) 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""" def on_modified(self, event): if event.src_path == CADDYFILE_PATH: logger.info(f"Caddyfile modified: {event.src_path}") 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 send_update(force=True) # Setup file watching 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.start() try: while True: # Periodic check even if file doesn't change time.sleep(CHECK_INTERVAL) send_update() except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": main()