This commit is contained in:
pika 2025-03-24 17:51:57 +01:00
parent a4ce8a291d
commit 950d72aba1
8 changed files with 375 additions and 22 deletions

138
app.py
View file

@ -1,39 +1,144 @@
from flask import Flask, render_template, request, jsonify
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<domains>[^\s{]+(?:,\s*[^\s{]+)*)\s*{.*?reverse_proxy\s+(?P<target>https?:\/\/[\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():
filtered_data = {k: v for k, v in proxy_data.items() if k not in deleted_servers}
return render_template('index.html', proxies=filtered_data)
"""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('/update', methods=['POST'])
@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"}), 400
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/<domain>')
@ -46,4 +151,19 @@ def check_status(domain):
return jsonify({"status": "offline"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
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)