"""HAProxy Config Generator - Build config from database""" import os import subprocess import shutil from datetime import datetime from database import db from database.models import VirtualHost, Certificate, HAPROXY_STATS_PORT from config.settings import HAPROXY_CONFIG_PATH, HAPROXY_BACKUP_DIR import logging logger = logging.getLogger(__name__) def generate_haproxy_config(): """Generate HAProxy config from database""" config_lines = [] # ===== GLOBAL SECTION ===== config_lines.extend([ "# HAProxy Configuration - Auto-generated by HAProxy Manager", f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "", "global", " log stdout local0", " log stdout local1 notice", " chroot /var/lib/haproxy", " stats socket /run/haproxy/admin.sock mode 660 level admin", " stats timeout 30s", " user haproxy", " group haproxy", " daemon", " maxconn 4096", " tune.ssl.default-dh-param 2048", "", ]) # ===== DEFAULTS SECTION ===== config_lines.extend([ "defaults", " log global", " mode http", " option httplog", " option dontlognull", " option http-server-close", " option forwardfor except 127.0.0.0/8", " option redispatch", " retries 3", " timeout connect 5000", " timeout client 50000", " timeout server 50000", " errorfile 400 /etc/haproxy/errors/400.http", " errorfile 403 /etc/haproxy/errors/403.http", " errorfile 408 /etc/haproxy/errors/408.http", " errorfile 500 /etc/haproxy/errors/500.http", " errorfile 502 /etc/haproxy/errors/502.http", " errorfile 503 /etc/haproxy/errors/503.http", " errorfile 504 /etc/haproxy/errors/504.http", "", ]) # ===== STATS FRONTEND (HARDCODED :8404) ===== config_lines.extend([ "# HAProxy Statistics - Hardcoded on port 8404", "frontend stats", f" bind *:{HAPROXY_STATS_PORT}", " stats enable", " stats admin if TRUE", " stats uri /stats", " stats refresh 30s", "", ]) # ===== FRONTENDS & BACKENDS FROM DATABASE ===== vhosts = VirtualHost.query.filter_by(enabled=True).all() for vhost in vhosts: # Skip if no backend servers if not vhost.backend_servers: logger.warning(f"[CONFIG] VHost '{vhost.name}' has no backend servers, skipping") continue # ===== FRONTEND ===== config_lines.append(f"# VHost: {vhost.name}") config_lines.append(f"frontend {vhost.name}") config_lines.append(f" description {vhost.hostname}") config_lines.append(f" bind {vhost.frontend_ip}:{vhost.frontend_port}") # SSL config if vhost.use_ssl and vhost.certificate_id: cert = Certificate.query.get(vhost.certificate_id) if cert: # Certificate file path cert_path = f"/app/uploads/certificates/{cert.name}.pem" config_lines.append(f" ssl crt {cert_path}") config_lines.append(f" mode {vhost.protocol}") config_lines.append(f" option httplog") # Custom headers if vhost.del_server_header: config_lines.append(" http-response del-header Server") if vhost.add_custom_header and vhost.custom_header_name: config_lines.append(f" http-response add-header {vhost.custom_header_name} {vhost.custom_header_value}") if vhost.forward_for: config_lines.append(" option forwardfor except 127.0.0.0/8") # Security rules if vhost.dos_protection: config_lines.extend([ f" stick-table type ip size 100k expire {vhost.dos_ban_duration} store http_req_rate(10s)", f" http-request track-sc0 src", f" http-request deny if {{ sc_http_req_rate(0) gt {vhost.dos_limit_requests} }}", ]) if vhost.sql_injection_check: config_lines.append(" # SQL Injection protection enabled") if vhost.xss_check: config_lines.append(" # XSS protection enabled") if vhost.webshell_check: config_lines.append(" # WebShell protection enabled") config_lines.append(f" default_backend {vhost.name}_backend") config_lines.append("") # ===== BACKEND ===== config_lines.append(f"backend {vhost.name}_backend") config_lines.append(f" balance {vhost.lb_method}") config_lines.append(f" option httpchk GET / HTTP/1.1\\r\\nHost:\\ www") # Backend servers for i, server in enumerate(vhost.backend_servers, 1): if not server.enabled: continue server_line = f" server {server.name} {server.ip_address}:{server.port}" if server.weight != 1: server_line += f" weight {server.weight}" if server.maxconn: server_line += f" maxconn {server.maxconn}" if server.health_check: server_line += f" check inter 5000 rise 2 fall 3" config_lines.append(server_line) config_lines.append("") return "\n".join(config_lines) def save_haproxy_config(config_content): """Save config to file""" try: # Create backup if os.path.exists(HAPROXY_CONFIG_PATH): timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_path = os.path.join(HAPROXY_BACKUP_DIR, f'haproxy_backup_{timestamp}.cfg') shutil.copy2(HAPROXY_CONFIG_PATH, backup_path) logger.info(f"[CONFIG] Backup created: {backup_path}", flush=True) # Write new config with open(HAPROXY_CONFIG_PATH, 'w') as f: f.write(config_content) logger.info(f"[CONFIG] Config saved to {HAPROXY_CONFIG_PATH}", flush=True) return True except Exception as e: logger.error(f"[CONFIG] Error saving config: {e}", flush=True) return False def reload_haproxy(): """Reload HAProxy service""" try: # Generate config config_content = generate_haproxy_config() # Test config syntax result = subprocess.run( ['haproxy', '-c', '-f', HAPROXY_CONFIG_PATH], capture_output=True, text=True, timeout=5 ) if result.returncode != 0: logger.error(f"[CONFIG] HAProxy syntax error: {result.stderr}", flush=True) return False # Save config if not save_haproxy_config(config_content): return False # Reload HAProxy result = subprocess.run( ['systemctl', 'reload', 'haproxy'], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: logger.info("[CONFIG] HAProxy reloaded successfully", flush=True) return True else: logger.error(f"[CONFIG] HAProxy reload failed: {result.stderr}", flush=True) return False except subprocess.TimeoutExpired: logger.error("[CONFIG] HAProxy reload timeout", flush=True) return False except Exception as e: logger.error(f"[CONFIG] Error reloading HAProxy: {e}", flush=True) return False