diff --git a/Dockerfile b/Dockerfile index 190e922..574b3ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,12 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . /app/ +# Create a volume for persistent data +VOLUME /app/data + EXPOSE 5000 +# Set environment variable for API key (should be overridden at runtime) +ENV CADDYDB_API_KEY="" + CMD ["python", "app.py"] diff --git a/app.py b/app.py index a911786..5fde4ef 100644 --- a/app.py +++ b/app.py @@ -1,33 +1,83 @@ -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, abort import requests import threading +import secrets +import os +import hashlib +import time +from functools import wraps app = Flask(__name__) +# Generate a secure API key if not already set in environment +if not os.environ.get('CADDYDB_API_KEY'): + app.config['API_KEY'] = os.environ.get('CADDYDB_API_KEY', secrets.token_hex(32)) + print(f"API Key: {app.config['API_KEY']} - Store this securely!") +else: + app.config['API_KEY'] = os.environ.get('CADDYDB_API_KEY') + proxy_data = {} deleted_servers = set() +# Authentication decorator +def require_api_key(f): + @wraps(f) + def decorated_function(*args, **kwargs): + api_key = request.headers.get('X-API-Key') + if not api_key or api_key != app.config['API_KEY']: + return jsonify({"error": "Unauthorized access"}), 401 + return f(*args, **kwargs) + return decorated_function + @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) -@app.route('/update', methods=['POST']) -def update(): +@app.route('/api/update', methods=['POST']) +@require_api_key +def api_update(): data = request.json if not data or "server" not in data or "entries" not in data: return jsonify({"error": "Invalid data"}), 400 - + server_name = data["server"] - proxy_data[server_name] = data["entries"] + source_type = data.get("source_type", "caddy") # Default to caddy if not specified + + proxy_data[server_name] = { + "entries": data["entries"], + "source_type": source_type, + "last_updated": time.time() + } if server_name in deleted_servers: deleted_servers.remove(server_name) return jsonify({"message": "Updated successfully"}), 200 -@app.route('/delete', methods=['POST']) -def delete_entry(): +# Legacy endpoint for backward compatibility +@app.route('/update', methods=['POST']) +@require_api_key +def update(): + data = request.json + if not data or "server" not in data or "entries" not in data: + return jsonify({"error": "Invalid data"}), 400 + + server_name = data["server"] + proxy_data[server_name] = { + "entries": data["entries"], + "source_type": "caddy", + "last_updated": time.time() + } + + if server_name in deleted_servers: + deleted_servers.remove(server_name) + + return jsonify({"message": "Updated successfully"}), 200 + +@app.route('/api/delete', methods=['POST']) +@require_api_key +def api_delete_entry(): data = request.json server_name = data.get("server") if not server_name or server_name not in proxy_data: @@ -36,8 +86,14 @@ def delete_entry(): deleted_servers.add(server_name) return jsonify({"message": f"Server {server_name} deleted"}), 200 -@app.route('/status/') -def check_status(domain): +# Legacy endpoint for backward compatibility +@app.route('/delete', methods=['POST']) +@require_api_key +def delete_entry(): + return api_delete_entry() + +@app.route('/api/status/') +def api_check_status(domain): """Check if a subdomain is reachable""" try: response = requests.get(f"https://{domain}", timeout=3) @@ -45,5 +101,17 @@ def check_status(domain): except requests.exceptions.RequestException: return jsonify({"status": "offline"}) +# Legacy endpoint for backward compatibility +@app.route('/status/') +def check_status(domain): + return api_check_status(domain) + +@app.route('/api/servers', methods=['GET']) +@require_api_key +def list_servers(): + """List all registered servers""" + filtered_data = {k: v for k, v in proxy_data.items() if k not in deleted_servers} + return jsonify({"servers": list(filtered_data.keys())}), 200 + if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) + app.run(host='0.0.0.0', port=5000, ssl_context='adhoc') # Using adhoc SSL for development diff --git a/compose.yml b/compose.yml index fef4c42..db5120c 100644 --- a/compose.yml +++ b/compose.yml @@ -2,4 +2,9 @@ services: caddydb: image: caddydb:latest ports: - - 5000:5000 + - "5000:5000" + environment: + - CADDYDB_API_KEY=${CADDYDB_API_KEY:-changeme} # Set this to a secure value + volumes: + - ./data:/app/data + restart: unless-stopped diff --git a/requirements.txt b/requirements.txt index 30692b7..2151e4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ flask requests +pyyaml +toml +pyopenssl diff --git a/src/addCrontab.sh b/src/addCrontab.sh index cf1a84e..4ce6101 100755 --- a/src/addCrontab.sh +++ b/src/addCrontab.sh @@ -5,10 +5,30 @@ command_exists() { command -v "$@" >/dev/null 2>&1 } -scriptPath="" +# Default values +scriptPath="/app/src/update.py" +apiKey="${CADDYDB_API_KEY:-}" +dashboardUrl="${CADDYDB_URL:-http://10.0.0.25:5000/api/update}" + +# Check if API key is set +if [ -z "$apiKey" ]; then + echo "Warning: CADDYDB_API_KEY environment variable is not set." + echo "Please set it before running this script." + exit 1 +fi if command_exists python3; then - echo "*/10 * * * * /usr/bin/python3 ${scriptPath}" | crontab - + # Create a wrapper script that includes the API key + wrapperScript="/tmp/caddydb_update_wrapper.sh" + echo "#!/bin/bash" > "$wrapperScript" + echo "export CADDYDB_API_KEY=\"$apiKey\"" >> "$wrapperScript" + echo "python3 $scriptPath --url $dashboardUrl" >> "$wrapperScript" + chmod +x "$wrapperScript" + + # Add to crontab + echo "*/10 * * * * $wrapperScript" | crontab - + echo "Added cron job to update CaddyDB every 10 minutes" else echo "No python was found.." + exit 1 fi diff --git a/src/nginx_update.py b/src/nginx_update.py new file mode 100644 index 0000000..b80d7a2 --- /dev/null +++ b/src/nginx_update.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import requests +import re +import socket +import os +import argparse +import sys +import glob + +# Default configuration +NGINX_CONF_DIR = "/etc/nginx/sites-enabled" +DASHBOARD_URL = "http://10.0.0.25:5000/api/update" +SERVER_NAME = socket.gethostname() +API_KEY = os.environ.get('CADDYDB_API_KEY', '') + +def parse_nginx_configs(conf_dir): + entries = {} + + # Get all config files + config_files = glob.glob(f"{conf_dir}/*") + + for config_file in config_files: + try: + with open(config_file, "r") as file: + content = file.read() + + # Extract server_name and proxy_pass + server_blocks = re.findall(r'server\s*{[^}]*}', content, re.DOTALL) + + for block in server_blocks: + server_names = re.search(r'server_name\s+([^;]+);', block) + proxy_pass = re.search(r'proxy_pass\s+([^;]+);', block) + + if server_names and proxy_pass: + domains = server_names.group(1).strip().split() + target = proxy_pass.group(1).strip() + + for domain in domains: + if domain != '_': # Skip default server + entries[domain] = target + except Exception as e: + print(f"Error reading Nginx config {config_file}: {e}") + + return entries + +def send_update(url, api_key, server_name, entries): + data = { + "server": server_name, + "entries": entries, + "source_type": "nginx" + } + + headers = {"X-API-Key": api_key} + + try: + response = requests.post(url, json=data, headers=headers) + if response.status_code == 200: + print(f"Successfully updated {server_name} data") + return True + else: + print(f"Error: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"Error sending update: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description='Update CaddyDB with Nginx reverse proxy entries') + parser.add_argument('--url', default=DASHBOARD_URL, help='CaddyDB API URL') + parser.add_argument('--dir', default=NGINX_CONF_DIR, help='Path to Nginx config directory') + parser.add_argument('--server', default=SERVER_NAME, help='Server name') + parser.add_argument('--key', default=API_KEY, help='API key') + + args = parser.parse_args() + + if not args.key: + print("Error: API key is required. Set CADDYDB_API_KEY environment variable or use --key") + sys.exit(1) + + entries = parse_nginx_configs(args.dir) + if not entries: + print("No entries found in Nginx configurations") + sys.exit(1) + + success = send_update(args.url, args.key, args.server, entries) + sys.exit(0 if success else 1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/traefik_update.py b/src/traefik_update.py new file mode 100644 index 0000000..f53991b --- /dev/null +++ b/src/traefik_update.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import requests +import socket +import os +import argparse +import sys +import yaml +import toml +import json + +# Default configuration +TRAEFIK_CONF_PATH = "/etc/traefik/traefik.yml" +DASHBOARD_URL = "http://10.0.0.25:5000/api/update" +SERVER_NAME = socket.gethostname() +API_KEY = os.environ.get('CADDYDB_API_KEY', '') + +def parse_traefik_config(conf_path): + entries = {} + + # Determine file type + if conf_path.endswith(('.yml', '.yaml')): + try: + with open(conf_path, 'r') as file: + config = yaml.safe_load(file) + except Exception as e: + print(f"Error reading YAML config: {e}") + return entries + elif conf_path.endswith('.toml'): + try: + with open(conf_path, 'r') as file: + config = toml.load(file) + except Exception as e: + print(f"Error reading TOML config: {e}") + return entries + elif conf_path.endswith('.json'): + try: + with open(conf_path, 'r') as file: + config = json.load(file) + except Exception as e: + print(f"Error reading JSON config: {e}") + return entries + else: + print(f"Unsupported file format: {conf_path}") + return entries + + # Try to extract router configurations + try: + # For static configuration + if 'http' in config and 'routers' in config['http']: + for router_name, router in config['http']['routers'].items(): + if 'rule' in router and 'service' in router: + # Extract host from rule (assuming Host rule) + host_match = router['rule'] + if 'Host(' in host_match: + domain = host_match.split('Host(')[1].split(')')[0].replace('`', '').strip() + service = router['service'] + entries[domain] = f"traefik service: {service}" + + # For dynamic file providers + if 'providers' in config and 'file' in config['providers']: + file_path = config['providers']['file'].get('filename') + if file_path: + # Recursively parse the dynamic config + dynamic_entries = parse_traefik_config(file_path) + entries.update(dynamic_entries) + except Exception as e: + print(f"Error parsing Traefik config: {e}") + + return entries + +def send_update(url, api_key, server_name, entries): + data = { + "server": server_name, + "entries": entries, + "source_type": "traefik" + } + + headers = {"X-API-Key": api_key} + + try: + response = requests.post(url, json=data, headers=headers) + if response.status_code == 200: + print(f"Successfully updated {server_name} data") + return True + else: + print(f"Error: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"Error sending update: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description='Update CaddyDB with Traefik reverse proxy entries') + parser.add_argument('--url', default=DASHBOARD_URL, help='CaddyDB API URL') + parser.add_argument('--config', default=TRAEFIK_CONF_PATH, help='Path to Traefik config file') + parser.add_argument('--server', default=SERVER_NAME, help='Server name') + parser.add_argument('--key', default=API_KEY, help='API key') + + args = parser.parse_args() + + if not args.key: + print("Error: API key is required. Set CADDYDB_API_KEY environment variable or use --key") + sys.exit(1) + + entries = parse_traefik_config(args.config) + if not entries: + print("No entries found in Traefik configuration") + sys.exit(1) + + success = send_update(args.url, args.key, args.server, entries) + sys.exit(0 if success else 1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/update.py b/src/update.py index 209dda8..b7abd05 100755 --- a/src/update.py +++ b/src/update.py @@ -2,17 +2,21 @@ import requests import re -# import os import socket +import os +import argparse +import sys -CADDYFILE_PATH = "/opt/docker/caddy/conf/Caddyfile" # Pfad zur Caddyfile -DASHBOARD_URL = "http://10.0.0.25:5000/update" # Anpassen! +# Default configuration +CADDYFILE_PATH = "/opt/docker/caddy/conf/Caddyfile" +DASHBOARD_URL = "http://10.0.0.25:5000/api/update" SERVER_NAME = socket.gethostname() +API_KEY = os.environ.get('CADDYDB_API_KEY', '') -def parse_caddyfile(): +def parse_caddyfile(file_path): entries = {} try: - with open(CADDYFILE_PATH, "r") as file: + with open(file_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) @@ -22,16 +26,51 @@ def parse_caddyfile(): for domain in domains.split(", "): entries[domain] = target.strip() except Exception as e: - print(f"Fehler beim Lesen der Caddyfile: {e}") + print(f"Error reading Caddyfile: {e}") return entries -def send_update(): - data = {"server": SERVER_NAME, "entries": parse_caddyfile()} +def send_update(url, api_key, server_name, entries, source_type="caddy"): + data = { + "server": server_name, + "entries": entries, + "source_type": source_type + } + + headers = {"X-API-Key": api_key} + try: - response = requests.post(DASHBOARD_URL, json=data) - print(response.json()) + response = requests.post(url, json=data, headers=headers) + if response.status_code == 200: + print(f"Successfully updated {server_name} data") + return True + else: + print(f"Error: {response.status_code} - {response.text}") + return False except Exception as e: - print(f"Fehler beim Senden: {e}") + print(f"Error sending update: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description='Update CaddyDB with reverse proxy entries') + parser.add_argument('--url', default=DASHBOARD_URL, help='CaddyDB API URL') + parser.add_argument('--file', default=CADDYFILE_PATH, help='Path to Caddyfile') + parser.add_argument('--server', default=SERVER_NAME, help='Server name') + parser.add_argument('--key', default=API_KEY, help='API key') + parser.add_argument('--source', default="caddy", help='Source type (caddy, nginx, traefik)') + + args = parser.parse_args() + + if not args.key: + print("Error: API key is required. Set CADDYDB_API_KEY environment variable or use --key") + sys.exit(1) + + entries = parse_caddyfile(args.file) + if not entries: + print("No entries found in Caddyfile") + sys.exit(1) + + success = send_update(args.url, args.key, args.server, entries, args.source) + sys.exit(0 if success else 1) if __name__ == "__main__": - send_update() + main() diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/index.html b/templates/index.html index 2671a91..a6cd9ba 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,6 @@ + @@ -26,8 +27,8 @@ function deleteServer(serverName) { fetch('/delete', { method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({"server": serverName}) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ "server": serverName }) }).then(response => { if (response.ok) { document.getElementById("server-box-" + serverName).remove(); @@ -35,7 +36,7 @@ }); } - document.addEventListener("keydown", function(event) { + document.addEventListener("keydown", function (event) { if (event.key === "/") { event.preventDefault(); toggleSearch(); @@ -43,29 +44,36 @@ }); +

Caddy Dashboard

Übersicht über aller aktiven Proxy-Server

- +
- + {% for server, entries in proxies.items() %}

{{ server }}

- +
{% for domain, target in entries.items() %} -
+

{{ target }}

@@ -77,4 +85,5 @@
- + + \ No newline at end of file