524 lines
19 KiB
Python
524 lines
19 KiB
Python
"""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
|