175 lines
6.1 KiB
Python
175 lines
6.1 KiB
Python
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')
|
|
DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true'
|
|
CADDYFILE_PATH = os.getenv('CADDYFILE_PATH') # No default - if not set, we don't use local file
|
|
|
|
# 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)
|
|
|
|
if CADDYFILE_PATH and not USE_LOCAL_CADDYFILE:
|
|
logger.warning(f"CADDYFILE_PATH is set but file not found at: {CADDYFILE_PATH}")
|
|
|
|
if not API_KEY:
|
|
logger.warning("API_KEY not set - running without authentication! This is insecure.")
|
|
|
|
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():
|
|
"""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/<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 __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)
|