286 lines
No EOL
9.7 KiB
Python
286 lines
No EOL
9.7 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import time
|
|
import json
|
|
import jwt
|
|
import requests
|
|
import socket
|
|
import re
|
|
import logging
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from watchdog.observers import Observer
|
|
from watchdog.events import FileSystemEventHandler
|
|
from dotenv import load_dotenv
|
|
import urllib3
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Fixed configuration
|
|
CADDYFILE_PATH = "/app/Caddyfile" # Fixed internal path
|
|
NGINX_CONFIG_PATH = "/app/nginx" # Fixed internal path for nginx configs
|
|
DASHBOARD_URL = os.getenv('DASHBOARD_URL', 'http://caddydb-server:5000/api/update')
|
|
SERVER_NAME = os.getenv('SERVER_NAME', socket.gethostname())
|
|
API_KEY = os.getenv('API_KEY')
|
|
CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '60'))
|
|
VERIFY_SSL = os.getenv('VERIFY_SSL', 'true').lower() == 'true'
|
|
SERVER_TYPE = os.getenv('SERVER_TYPE', 'caddy').lower() # 'caddy' or 'nginx'
|
|
|
|
# Setup logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger('caddy-agent')
|
|
|
|
# Debug configuration
|
|
logger.info(f"Starting {SERVER_TYPE} agent with configuration:")
|
|
logger.info(f"- DASHBOARD_URL: {DASHBOARD_URL}")
|
|
logger.info(f"- SERVER_NAME: {SERVER_NAME}")
|
|
logger.info(f"- CADDYFILE_PATH: {CADDYFILE_PATH}")
|
|
logger.info(f"- SERVER_TYPE: {SERVER_TYPE}")
|
|
logger.info(f"- VERIFY_SSL: {VERIFY_SSL}")
|
|
logger.info(f"- API_KEY set: {'Yes' if API_KEY else 'No'}")
|
|
|
|
# Disable SSL warnings if verification is disabled
|
|
if not VERIFY_SSL:
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
logger.warning("SSL verification is disabled - this is insecure!")
|
|
|
|
# Validate required configuration
|
|
if not os.path.exists(CADDYFILE_PATH):
|
|
logger.error(f"Caddyfile not found at {CADDYFILE_PATH}")
|
|
sys.exit(1)
|
|
|
|
if not DASHBOARD_URL:
|
|
logger.error("DASHBOARD_URL environment variable not set - cannot send updates")
|
|
sys.exit(1)
|
|
|
|
if not API_KEY:
|
|
logger.warning("API_KEY environment variable not set - authentication will fail!")
|
|
|
|
# Last data sent to avoid unnecessary updates
|
|
last_data_sent = None
|
|
last_send_time = datetime.min
|
|
|
|
# Flag to determine what type of config to monitor
|
|
IS_NGINX = SERVER_TYPE.lower() == 'nginx'
|
|
|
|
def parse_caddyfile():
|
|
"""Parse the Caddyfile to extract domains and their proxy targets"""
|
|
entries = {}
|
|
try:
|
|
if not os.path.exists(CADDYFILE_PATH):
|
|
logger.error(f"Caddyfile not found at {CADDYFILE_PATH}")
|
|
return entries
|
|
|
|
with open(CADDYFILE_PATH, "r") as file:
|
|
content = file.read()
|
|
|
|
# Revert to simpler pattern that was working previously
|
|
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)
|
|
|
|
logger.info(f"Found {len(matches)} matches in Caddyfile")
|
|
|
|
for domains, target in matches:
|
|
for domain in re.split(r',\s*', domains):
|
|
domain = domain.strip()
|
|
if domain and domain.lower() != "host": # Skip entries named "host"
|
|
entries[domain] = target.strip()
|
|
|
|
logger.info(f"Extracted {len(entries)} domain entries from Caddyfile")
|
|
|
|
# Debug output of parsed entries
|
|
for domain, target in entries.items():
|
|
logger.debug(f"Domain: {domain} -> {target}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing Caddyfile: {e}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|
|
|
|
return entries
|
|
|
|
def create_auth_token():
|
|
"""Create a JWT token for authentication"""
|
|
if not API_KEY:
|
|
logger.error("Cannot create authentication token: API_KEY not set")
|
|
return None
|
|
|
|
# Create token with 5 minute expiry
|
|
payload = {
|
|
"exp": datetime.utcnow() + timedelta(minutes=5),
|
|
"iat": datetime.utcnow(),
|
|
"sub": SERVER_NAME
|
|
}
|
|
|
|
try:
|
|
token = jwt.encode(payload, API_KEY, algorithm="HS256")
|
|
return token
|
|
except Exception as e:
|
|
logger.error(f"Error creating JWT token: {e}")
|
|
return None
|
|
|
|
def parse_nginx_configs():
|
|
"""Parse Nginx config files to extract domains and their proxy targets"""
|
|
entries = {}
|
|
|
|
if not os.path.exists(NGINX_CONFIG_PATH) or not os.path.isdir(NGINX_CONFIG_PATH):
|
|
logger.error(f"Nginx config directory not found at {NGINX_CONFIG_PATH}")
|
|
return entries
|
|
|
|
try:
|
|
# Find all .conf files in the directory and subdirectories
|
|
conf_files = []
|
|
for root, _, files in os.walk(NGINX_CONFIG_PATH):
|
|
for file in files:
|
|
if file.endswith('.conf'):
|
|
conf_files.append(os.path.join(root, file))
|
|
|
|
logger.info(f"Found {len(conf_files)} Nginx config files")
|
|
|
|
# Pattern to match server_name and proxy_pass directives
|
|
server_name_pattern = re.compile(r'server_name\s+([^;]+);', re.IGNORECASE)
|
|
proxy_pass_pattern = re.compile(r'proxy_pass\s+([^;]+);', re.IGNORECASE)
|
|
|
|
for conf_file in conf_files:
|
|
try:
|
|
with open(conf_file, 'r') as file:
|
|
content = file.read()
|
|
|
|
# Extract server blocks
|
|
server_blocks = re.findall(r'server\s*{([^}]+)}', content, re.DOTALL)
|
|
|
|
for block in server_blocks:
|
|
server_names = server_name_pattern.search(block)
|
|
proxy_pass = proxy_pass_pattern.search(block)
|
|
|
|
if server_names and proxy_pass:
|
|
server_names = server_names.group(1).strip().split()
|
|
target = proxy_pass.group(1).strip()
|
|
|
|
for name in server_names:
|
|
# Skip default names like "_" or localhost
|
|
if name != "_" and name != "localhost" and name != "localhost.localdomain":
|
|
entries[name] = target
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing Nginx config file {conf_file}: {e}")
|
|
|
|
return entries
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing Nginx configs: {e}")
|
|
return entries
|
|
|
|
def send_update(force=False):
|
|
"""Send configuration data to the dashboard server"""
|
|
global last_data_sent, last_send_time
|
|
|
|
# Parse the appropriate configuration
|
|
if IS_NGINX:
|
|
current_data = parse_nginx_configs()
|
|
else:
|
|
current_data = parse_caddyfile()
|
|
|
|
current_time = datetime.now()
|
|
|
|
# Only send if data changed or enough time passed since last update
|
|
if (not force and
|
|
current_data == last_data_sent and
|
|
(current_time - last_send_time).total_seconds() < CHECK_INTERVAL):
|
|
return
|
|
|
|
# Create the data payload
|
|
data = {
|
|
"server": SERVER_NAME,
|
|
"entries": current_data,
|
|
"timestamp": current_time.isoformat(),
|
|
"type": SERVER_TYPE
|
|
}
|
|
|
|
# Create authentication token
|
|
token = create_auth_token()
|
|
if not token:
|
|
return
|
|
|
|
# Prepare headers with authentication
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': f'Bearer {token}'
|
|
}
|
|
|
|
try:
|
|
response = requests.post(
|
|
DASHBOARD_URL,
|
|
json=data,
|
|
headers=headers,
|
|
timeout=10,
|
|
verify=VERIFY_SSL
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
logger.info(f"Update sent successfully: {response.json()}")
|
|
last_data_sent = current_data
|
|
last_send_time = current_time
|
|
else:
|
|
logger.error(f"Error sending update: {response.status_code} - {response.text}")
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Connection error sending update: {e}")
|
|
|
|
class ConfigFileHandler(FileSystemEventHandler):
|
|
"""Watch for changes to configuration files and send updates"""
|
|
def on_modified(self, event):
|
|
if IS_NGINX:
|
|
# For Nginx, check if it's a .conf file in the watched directory
|
|
if event.src_path.endswith('.conf'):
|
|
logger.info(f"Nginx config changed: {event.src_path}, sending update")
|
|
send_update()
|
|
else:
|
|
# For Caddy, check if it's the Caddyfile
|
|
if event.src_path == CADDYFILE_PATH:
|
|
logger.info(f"Caddyfile changed, sending update")
|
|
send_update()
|
|
|
|
def main():
|
|
"""Main function to start the agent"""
|
|
# Send initial update
|
|
send_update(force=True)
|
|
|
|
# Set up file watching
|
|
event_handler = ConfigFileHandler()
|
|
observer = Observer()
|
|
|
|
if IS_NGINX:
|
|
# Watch the Nginx config directory
|
|
if not os.path.exists(NGINX_CONFIG_PATH):
|
|
logger.error(f"Nginx config path not found: {NGINX_CONFIG_PATH}")
|
|
sys.exit(1)
|
|
observer.schedule(event_handler, path=NGINX_CONFIG_PATH, recursive=True)
|
|
logger.info(f"Watching Nginx configs in {NGINX_CONFIG_PATH} for changes")
|
|
else:
|
|
# Watch the Caddyfile
|
|
if not os.path.exists(CADDYFILE_PATH):
|
|
logger.error(f"Caddyfile not found: {CADDYFILE_PATH}")
|
|
sys.exit(1)
|
|
observer.schedule(event_handler, path=os.path.dirname(CADDYFILE_PATH), recursive=False)
|
|
logger.info(f"Watching {CADDYFILE_PATH} for changes")
|
|
|
|
observer.start()
|
|
|
|
try:
|
|
logger.info(f"{SERVER_TYPE.capitalize()} agent started successfully")
|
|
while True:
|
|
# Send periodic updates
|
|
send_update()
|
|
time.sleep(CHECK_INTERVAL)
|
|
except KeyboardInterrupt:
|
|
observer.stop()
|
|
|
|
observer.join()
|
|
|
|
if __name__ == "__main__":
|
|
main() |