diff --git a/agent.py b/agent.py index 495f6f8..b7a9226 100644 --- a/agent.py +++ b/agent.py @@ -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__": diff --git a/app.py b/app.py index 873b190..9272587 100644 --- a/app.py +++ b/app.py @@ -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/') -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!") diff --git a/compose.yml b/compose.yml index 9632621..10da1f0 100644 --- a/compose.yml +++ b/compose.yml @@ -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: diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..ecadceb --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,149 @@ + + + + + + + Caddy Dashboard + + + + + + + +
+
+

Caddy Dashboard

+ +
+
+ +
+ +
+
+
+
+ +
+
+

Caddy Servers

+

{{ caddy_count }}

+
+
+
+ +
+
+
+ +
+
+

Nginx Servers

+

{{ nginx_count }}

+
+
+
+ +
+
+
+ +
+
+

Total Domains

+

{{ domain_count }}

+
+
+
+ +
+
+
+ +
+
+

Last Update

+

{{ last_update }}

+
+
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index db5a850..cc1779b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ - Caddy Dashboard + {{ server_type }} Dashboard