This commit is contained in:
Mateusz Gruszczyński
2025-11-04 09:56:37 +01:00
parent 32ef62e4ac
commit addb21bc3e
34 changed files with 3864 additions and 367 deletions

523
routes/vhost_routes.py Normal file
View File

@@ -0,0 +1,523 @@
"""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('/<int:vhost_id>', 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('/<int:vhost_id>', 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('/<int:vhost_id>', 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('/<int:vhost_id>/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('/<int:vhost_id>/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('/<int:vhost_id>/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/<int:server_id>', 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/<int:server_id>', 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