from flask import Flask, render_template, request, jsonify, abort import requests import threading import os import jwt import logging import json from datetime import datetime from dotenv import load_dotenv # Load environment variables load_dotenv() # Configuration API_KEY = os.getenv('API_KEY') # Must match agent configuration DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true' CADDYFILE_PATH = os.getenv('CADDYFILE_PATH') # Optional - for direct file reading # Setup logging logging.basicConfig( level=logging.DEBUG if DEBUG_MODE else logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('caddy-dashboard') # Determine if we should use local Caddyfile reading USE_LOCAL_CADDYFILE = CADDYFILE_PATH and os.path.exists(CADDYFILE_PATH) 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 def verify_token(token): """Verify the JWT token from an agent""" if not API_KEY: logger.error("API_KEY is not configured - authentication disabled") return None try: return jwt.decode(token, API_KEY, algorithms=['HS256']) except jwt.ExpiredSignatureError: logger.warning("Authentication token has expired") return None except jwt.InvalidTokenError as e: logger.warning(f"Invalid authentication token: {e}") return None def parse_local_caddyfile(): """Parse a local Caddyfile if LOCAL_MODE is enabled""" if not CADDYFILE_PATH or not os.path.exists(CADDYFILE_PATH): logger.error(f"Local Caddyfile not found at {CADDYFILE_PATH}") return {} # Import here to avoid circular imports import re entries = {} try: 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: entries[domain] = target.strip() logger.info(f"Found {len(entries)} domain entries in local Caddyfile") except Exception as e: logger.error(f"Error parsing local Caddyfile: {e}") return entries @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) @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) if not payload: return jsonify({"error": "Invalid authentication token"}), 401 # Verify the server in the token matches the data data = request.json if not data or "server" not in data or "entries" not in data: return jsonify({"error": "Invalid data format"}), 400 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 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") 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 @app.route('/delete', methods=['POST']) def delete_entry(): """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 deleted_servers.add(server_name) logger.info(f"Server {server_name} marked as deleted") return jsonify({"message": f"Server {server_name} deleted"}), 200 @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 __name__ == '__main__': if USE_LOCAL_CADDYFILE: logger.info(f"Local Caddyfile found at {CADDYFILE_PATH} - will display its data") # 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") if not API_KEY: logger.warning("API_KEY not set - running without authentication!") # Use HTTPS in production if DEBUG_MODE: app.run(host='0.0.0.0', port=5000, debug=True) else: app.run(host='0.0.0.0', port=5000)