This commit is contained in:
pika 2025-03-24 19:58:23 +01:00
parent 82b2885576
commit fd5840df94
5 changed files with 394 additions and 104 deletions

228
app.py
View file

@ -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/<domain>')
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!")