"""Virtual Host Management - RESTful API""" from flask import Blueprint, request, jsonify, session from functools import wraps from database import db from database.models import VirtualHost, BackendServer, ConfigHistory, Certificate from utils.config_generator import generate_haproxy_config, reload_haproxy from datetime import datetime import logging vhost_bp = Blueprint('vhosts', __name__, url_prefix='/api/vhosts') logger = logging.getLogger(__name__) def login_required_api(f): """API version of login required""" @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return jsonify({'error': 'Not authenticated', 'success': False}), 401 return f(*args, **kwargs) return decorated_function @vhost_bp.route('', methods=['GET']) @login_required_api def list_vhosts(): """Get all virtual hosts""" try: vhosts = VirtualHost.query.order_by(VirtualHost.created_at.desc()).all() return jsonify({ 'success': True, 'vhosts': [{ 'id': v.id, 'name': v.name, 'hostname': v.hostname, 'frontend_ip': v.frontend_ip, 'frontend_port': v.frontend_port, 'protocol': v.protocol, 'use_ssl': v.use_ssl, 'lb_method': v.lb_method, 'enabled': v.enabled, 'backend_count': len(v.backend_servers), 'created_at': v.created_at.isoformat(), 'updated_at': v.updated_at.isoformat() if v.updated_at else None } for v in vhosts] }) except Exception as e: logger.error(f"[VHOSTS] Error listing: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('/', methods=['GET']) @login_required_api def get_vhost(vhost_id): """Get single vhost with backend servers""" try: vhost = VirtualHost.query.get(vhost_id) if not vhost: return jsonify({'error': 'VHost not found', 'success': False}), 404 return jsonify({ 'success': True, 'vhost': { 'id': vhost.id, 'name': vhost.name, 'hostname': vhost.hostname, 'description': vhost.description, 'frontend_ip': vhost.frontend_ip, 'frontend_port': vhost.frontend_port, 'protocol': vhost.protocol, 'use_ssl': vhost.use_ssl, 'certificate_id': vhost.certificate_id, 'ssl_redirect': vhost.ssl_redirect, 'ssl_redirect_port': vhost.ssl_redirect_port, 'lb_method': vhost.lb_method, 'dos_protection': vhost.dos_protection, 'dos_ban_duration': vhost.dos_ban_duration, 'dos_limit_requests': vhost.dos_limit_requests, 'sql_injection_check': vhost.sql_injection_check, 'xss_check': vhost.xss_check, 'webshell_check': vhost.webshell_check, 'add_custom_header': vhost.add_custom_header, 'custom_header_name': vhost.custom_header_name, 'custom_header_value': vhost.custom_header_value, 'del_server_header': vhost.del_server_header, 'forward_for': vhost.forward_for, 'enabled': vhost.enabled, 'backend_servers': [{ 'id': bs.id, 'name': bs.name, 'ip_address': bs.ip_address, 'port': bs.port, 'maxconn': bs.maxconn, 'weight': bs.weight, 'health_check': bs.health_check, 'health_check_path': bs.health_check_path, 'enabled': bs.enabled } for bs in vhost.backend_servers] } }) except Exception as e: logger.error(f"[VHOSTS] Error getting vhost {vhost_id}: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('', methods=['POST']) @login_required_api def create_vhost(): """Create new virtual host""" try: data = request.json # Validate required fields required = ['name', 'hostname', 'frontend_port'] for field in required: if not data.get(field): return jsonify({'error': f'{field} is required', 'success': False}), 400 # Check if name already exists if VirtualHost.query.filter_by(name=data['name']).first(): return jsonify({'error': 'VHost name already exists', 'success': False}), 400 # Create vhost vhost = VirtualHost( name=data['name'].strip(), hostname=data['hostname'].strip(), description=data.get('description', '').strip(), frontend_ip=data.get('frontend_ip', '0.0.0.0'), frontend_port=int(data['frontend_port']), protocol=data.get('protocol', 'http'), use_ssl=data.get('use_ssl', False), certificate_id=data.get('certificate_id'), ssl_redirect=data.get('ssl_redirect', False), ssl_redirect_port=int(data.get('ssl_redirect_port', 80)), lb_method=data.get('lb_method', 'roundrobin'), dos_protection=data.get('dos_protection', False), dos_ban_duration=data.get('dos_ban_duration', '30m'), dos_limit_requests=int(data.get('dos_limit_requests', 100)), sql_injection_check=data.get('sql_injection_check', False), xss_check=data.get('xss_check', False), webshell_check=data.get('webshell_check', False), add_custom_header=data.get('add_custom_header', False), custom_header_name=data.get('custom_header_name', ''), custom_header_value=data.get('custom_header_value', ''), del_server_header=data.get('del_server_header', False), forward_for=data.get('forward_for', True), enabled=data.get('enabled', True) ) db.session.add(vhost) db.session.flush() # Add backend servers if provided if data.get('backend_servers'): for bs_data in data['backend_servers']: backend = BackendServer( vhost_id=vhost.id, name=bs_data.get('name', f'server_{bs_data.get("ip_address")}'), ip_address=bs_data['ip_address'], port=int(bs_data['port']), maxconn=bs_data.get('maxconn'), weight=int(bs_data.get('weight', 1)), health_check=bs_data.get('health_check', False), health_check_path=bs_data.get('health_check_path', '/'), enabled=bs_data.get('enabled', True) ) db.session.add(backend) db.session.commit() # Save config history config_history = ConfigHistory( config_content=generate_haproxy_config(), change_type='vhost_create', vhost_id=vhost.id, user_id=session['user_id'], description=f"Created VHost: {vhost.name}" ) db.session.add(config_history) db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[VHOSTS] Created VHost '{vhost.name}' by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'id': vhost.id, 'name': vhost.name, 'message': 'VHost created successfully' }), 201 except Exception as e: db.session.rollback() logger.error(f"[VHOSTS] Error creating vhost: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('/', methods=['PUT']) @login_required_api def update_vhost(vhost_id): """Update virtual host""" try: vhost = VirtualHost.query.get(vhost_id) if not vhost: return jsonify({'error': 'VHost not found', 'success': False}), 404 data = request.json # Update basic fields if 'name' in data: # Check if name is taken existing = VirtualHost.query.filter_by(name=data['name']).filter(VirtualHost.id != vhost_id).first() if existing: return jsonify({'error': 'VHost name already exists', 'success': False}), 400 vhost.name = data['name'].strip() if 'hostname' in data: vhost.hostname = data['hostname'].strip() if 'description' in data: vhost.description = data['description'].strip() if 'frontend_ip' in data: vhost.frontend_ip = data['frontend_ip'] if 'frontend_port' in data: vhost.frontend_port = int(data['frontend_port']) if 'protocol' in data: vhost.protocol = data['protocol'] if 'use_ssl' in data: vhost.use_ssl = data['use_ssl'] if 'certificate_id' in data: vhost.certificate_id = data['certificate_id'] if 'ssl_redirect' in data: vhost.ssl_redirect = data['ssl_redirect'] if 'ssl_redirect_port' in data: vhost.ssl_redirect_port = int(data['ssl_redirect_port']) if 'lb_method' in data: vhost.lb_method = data['lb_method'] # Security settings if 'dos_protection' in data: vhost.dos_protection = data['dos_protection'] if 'dos_ban_duration' in data: vhost.dos_ban_duration = data['dos_ban_duration'] if 'dos_limit_requests' in data: vhost.dos_limit_requests = int(data['dos_limit_requests']) if 'sql_injection_check' in data: vhost.sql_injection_check = data['sql_injection_check'] if 'xss_check' in data: vhost.xss_check = data['xss_check'] if 'webshell_check' in data: vhost.webshell_check = data['webshell_check'] # Header settings if 'add_custom_header' in data: vhost.add_custom_header = data['add_custom_header'] if 'custom_header_name' in data: vhost.custom_header_name = data['custom_header_name'] if 'custom_header_value' in data: vhost.custom_header_value = data['custom_header_value'] if 'del_server_header' in data: vhost.del_server_header = data['del_server_header'] if 'forward_for' in data: vhost.forward_for = data['forward_for'] if 'enabled' in data: vhost.enabled = data['enabled'] vhost.updated_at = datetime.utcnow() db.session.commit() # Save config history config_history = ConfigHistory( config_content=generate_haproxy_config(), change_type='vhost_edit', vhost_id=vhost.id, user_id=session['user_id'], description=f"Updated VHost: {vhost.name}" ) db.session.add(config_history) db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[VHOSTS] Updated VHost '{vhost.name}' by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'message': 'VHost updated successfully' }) except Exception as e: db.session.rollback() logger.error(f"[VHOSTS] Error updating vhost {vhost_id}: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('/', methods=['DELETE']) @login_required_api def delete_vhost(vhost_id): """Delete virtual host""" try: vhost = VirtualHost.query.get(vhost_id) if not vhost: return jsonify({'error': 'VHost not found', 'success': False}), 404 vhost_name = vhost.name # Save config history before deletion config_history = ConfigHistory( config_content=generate_haproxy_config(), change_type='vhost_delete', vhost_id=vhost.id, user_id=session['user_id'], description=f"Deleted VHost: {vhost_name}" ) db.session.add(config_history) # Delete vhost (cascades to backend servers) db.session.delete(vhost) db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[VHOSTS] Deleted VHost '{vhost_name}' by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'message': f'VHost {vhost_name} deleted successfully' }) except Exception as e: db.session.rollback() logger.error(f"[VHOSTS] Error deleting vhost {vhost_id}: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('//toggle', methods=['POST']) @login_required_api def toggle_vhost(vhost_id): """Toggle vhost enabled/disabled""" try: vhost = VirtualHost.query.get(vhost_id) if not vhost: return jsonify({'error': 'VHost not found', 'success': False}), 404 vhost.enabled = not vhost.enabled vhost.updated_at = datetime.utcnow() db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[VHOSTS] Toggled VHost '{vhost.name}' to {vhost.enabled} by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'enabled': vhost.enabled, 'message': f"VHost {'enabled' if vhost.enabled else 'disabled'}" }) except Exception as e: db.session.rollback() logger.error(f"[VHOSTS] Error toggling vhost {vhost_id}: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 # ===== BACKEND SERVERS ===== @vhost_bp.route('//servers', methods=['GET']) @login_required_api def get_vhost_servers(vhost_id): """Get all backend servers for vhost""" try: vhost = VirtualHost.query.get(vhost_id) if not vhost: return jsonify({'error': 'VHost not found', 'success': False}), 404 return jsonify({ 'success': True, 'servers': [{ 'id': bs.id, 'name': bs.name, 'ip_address': bs.ip_address, 'port': bs.port, 'maxconn': bs.maxconn, 'weight': bs.weight, 'health_check': bs.health_check, 'health_check_path': bs.health_check_path, 'enabled': bs.enabled } for bs in vhost.backend_servers] }) except Exception as e: logger.error(f"[SERVERS] Error getting servers: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('//servers', methods=['POST']) @login_required_api def add_backend_server(vhost_id): """Add backend server to vhost""" try: vhost = VirtualHost.query.get(vhost_id) if not vhost: return jsonify({'error': 'VHost not found', 'success': False}), 404 data = request.json if not data.get('ip_address') or not data.get('port'): return jsonify({'error': 'IP address and port required', 'success': False}), 400 server = BackendServer( vhost_id=vhost_id, name=data.get('name', f"server_{data['ip_address']}"), ip_address=data['ip_address'], port=int(data['port']), maxconn=data.get('maxconn'), weight=int(data.get('weight', 1)), health_check=data.get('health_check', False), health_check_path=data.get('health_check_path', '/'), enabled=data.get('enabled', True) ) db.session.add(server) db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[SERVERS] Added server to VHost '{vhost.name}' by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'id': server.id, 'message': 'Backend server added' }), 201 except Exception as e: db.session.rollback() logger.error(f"[SERVERS] Error adding server: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('/servers/', methods=['PUT']) @login_required_api def update_backend_server(server_id): """Update backend server""" try: server = BackendServer.query.get(server_id) if not server: return jsonify({'error': 'Server not found', 'success': False}), 404 data = request.json if 'name' in data: server.name = data['name'] if 'ip_address' in data: server.ip_address = data['ip_address'] if 'port' in data: server.port = int(data['port']) if 'maxconn' in data: server.maxconn = data['maxconn'] if 'weight' in data: server.weight = int(data['weight']) if 'health_check' in data: server.health_check = data['health_check'] if 'health_check_path' in data: server.health_check_path = data['health_check_path'] if 'enabled' in data: server.enabled = data['enabled'] server.updated_at = datetime.utcnow() db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[SERVERS] Updated server '{server.name}' by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'message': 'Backend server updated' }) except Exception as e: db.session.rollback() logger.error(f"[SERVERS] Error updating server: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500 @vhost_bp.route('/servers/', methods=['DELETE']) @login_required_api def delete_backend_server(server_id): """Delete backend server""" try: server = BackendServer.query.get(server_id) if not server: return jsonify({'error': 'Server not found', 'success': False}), 404 server_name = server.name vhost_id = server.vhost_id db.session.delete(server) db.session.commit() # Reload HAProxy reload_haproxy() logger.info(f"[SERVERS] Deleted server '{server_name}' by {session.get('username')}", flush=True) return jsonify({ 'success': True, 'message': f'Backend server {server_name} deleted' }) except Exception as e: db.session.rollback() logger.error(f"[SERVERS] Error deleting server: {e}", flush=True) return jsonify({'error': str(e), 'success': False}), 500