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

27
database/__init__.py Normal file
View File

@@ -0,0 +1,27 @@
"""Database initialization"""
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
def init_db(app):
"""Initialize database with app"""
db.init_app(app)
migrate.init_app(app, db)
# Create tables
with app.app_context():
db.create_all()
# Create default admin user if not exists
from database.models import User
admin = User.query.filter_by(username='admin').first()
if not admin:
from config.settings import DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD
admin = User(username=DEFAULT_ADMIN_USERNAME)
admin.set_password(DEFAULT_ADMIN_PASSWORD)
db.session.add(admin)
db.session.commit()
print(f"[DB] Created default admin user: {DEFAULT_ADMIN_USERNAME}", flush=True)

83
database/migration.py Normal file
View File

@@ -0,0 +1,83 @@
"""Migration: Import existing HAProxy config to database"""
import re
from database import db
from database.models import VirtualHost, BackendServer
def parse_existing_haproxy_config(config_path):
"""Parse existing haproxy.cfg and import to DB"""
try:
with open(config_path, 'r') as f:
config_content = f.read()
print(f"[MIGRATION] Parsing {config_path}...")
# Pattern: frontend FNAME ...
frontend_pattern = r'frontend\s+(\S+)\s*\n(.*?)(?=\n\nbackend|\Z)'
frontends = re.findall(frontend_pattern, config_content, re.DOTALL)
vhost_count = 0
for fe_name, fe_content in frontends:
# Skip stats frontend
if 'stats' in fe_name.lower():
continue
# Extract bind
bind_match = re.search(r'bind\s+([^\s:]+):(\d+)', fe_content)
if not bind_match:
continue
ip, port = bind_match.groups()
# Extract backend name
backend_match = re.search(r'default_backend\s+(\S+)', fe_content)
backend_name = backend_match.group(1) if backend_match else f'be_{fe_name}'
# Check if already exists
vhost = VirtualHost.query.filter_by(name=fe_name).first()
if vhost:
print(f"[MIGRATION] Vhost '{fe_name}' already exists, skipping...")
continue
# Create vhost
vhost = VirtualHost(
name=fe_name,
hostname=f"{fe_name}.local",
frontend_ip=ip,
frontend_port=int(port),
protocol='http',
use_ssl='ssl' in fe_content,
enabled=True
)
db.session.add(vhost)
db.session.flush() # Get vhost.id
# Parse backend servers
be_pattern = rf'backend\s+{re.escape(backend_name)}\s*\n(.*?)(?=\nbackend|\Z)'
be_match = re.search(be_pattern, config_content, re.DOTALL)
if be_match:
servers_pattern = r'server\s+(\S+)\s+([^\s:]+):(\d+)'
servers = re.findall(servers_pattern, be_match.group(1))
for srv_name, srv_ip, srv_port in servers:
server = BackendServer(
vhost_id=vhost.id,
name=srv_name,
ip_address=srv_ip,
port=int(srv_port),
enabled=True
)
db.session.add(server)
vhost_count += 1
db.session.commit()
print(f"[MIGRATION] Successfully imported {vhost_count} vhosts!", flush=True)
return vhost_count
except Exception as e:
db.session.rollback()
print(f"[MIGRATION] Error: {e}", flush=True)
return 0

170
database/modelspy Normal file
View File

@@ -0,0 +1,170 @@
"""Database Models"""
from datetime import datetime
from database import db
from werkzeug.security import generate_password_hash, check_password_hash
import json
class User(db.Model):
"""User model for authentication"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
def set_password(self, password):
"""Hash and set password"""
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
def check_password(self, password):
"""Verify password"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
class Certificate(db.Model):
"""SSL Certificate storage"""
__tablename__ = 'certificates'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False, unique=True, index=True)
cert_content = db.Column(db.Text, nullable=False) # Full PEM (cert + key combined)
cert_only = db.Column(db.Text) # Separate cert (for info)
key_only = db.Column(db.Text) # Separate key (for backup)
# Metadata
common_name = db.Column(db.String(255))
subject_alt_names = db.Column(db.Text) # JSON array
issued_at = db.Column(db.DateTime)
expires_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
# Relationships
vhosts = db.relationship('VirtualHost', backref='certificate', lazy=True)
def get_san_list(self):
"""Get Subject Alternative Names as list"""
if self.subject_alt_names:
try:
return json.loads(self.subject_alt_names)
except:
return []
return []
def set_san_list(self, san_list):
"""Set Subject Alternative Names from list"""
self.subject_alt_names = json.dumps(san_list)
def __repr__(self):
return f'<Certificate {self.name}>'
class VirtualHost(db.Model):
"""Virtual Host / Proxy configuration"""
__tablename__ = 'virtual_hosts'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False, unique=True, index=True)
hostname = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
# ===== FRONTEND SETTINGS =====
frontend_ip = db.Column(db.String(50), default='0.0.0.0')
frontend_port = db.Column(db.Integer, default=443)
protocol = db.Column(db.String(10), default='http') # http or tcp
# ===== SSL SETTINGS =====
use_ssl = db.Column(db.Boolean, default=False)
certificate_id = db.Column(db.Integer, db.ForeignKey('certificates.id'))
ssl_redirect = db.Column(db.Boolean, default=False)
ssl_redirect_port = db.Column(db.Integer, default=80)
# ===== LOAD BALANCING =====
lb_method = db.Column(db.String(50), default='roundrobin') # roundrobin, leastconn, source, uri
# ===== SECURITY OPTIONS =====
dos_protection = db.Column(db.Boolean, default=False)
dos_ban_duration = db.Column(db.String(20), default='30m')
dos_limit_requests = db.Column(db.Integer, default=100)
sql_injection_check = db.Column(db.Boolean, default=False)
xss_check = db.Column(db.Boolean, default=False)
webshell_check = db.Column(db.Boolean, default=False)
# ===== HEADERS =====
add_custom_header = db.Column(db.Boolean, default=False)
custom_header_name = db.Column(db.String(200))
custom_header_value = db.Column(db.String(500))
del_server_header = db.Column(db.Boolean, default=False)
forward_for = db.Column(db.Boolean, default=True)
# ===== STATE =====
enabled = db.Column(db.Boolean, default=True, index=True)
# ===== TIMESTAMPS =====
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
# Relationships
backend_servers = db.relationship('BackendServer', backref='vhost',
lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<VirtualHost {self.name}>'
class BackendServer(db.Model):
"""Backend server for virtual host"""
__tablename__ = 'backend_servers'
id = db.Column(db.Integer, primary_key=True)
vhost_id = db.Column(db.Integer, db.ForeignKey('virtual_hosts.id'), nullable=False)
# Server info
name = db.Column(db.String(100), nullable=False)
ip_address = db.Column(db.String(50), nullable=False)
port = db.Column(db.Integer, nullable=False)
maxconn = db.Column(db.Integer)
weight = db.Column(db.Integer, default=1)
# Health check
health_check = db.Column(db.Boolean, default=False)
health_check_path = db.Column(db.String(200), default='/')
# State
enabled = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
def __repr__(self):
return f'<BackendServer {self.name}:{self.port}>'
class ConfigHistory(db.Model):
"""History of HAProxy configuration changes"""
__tablename__ = 'config_history'
id = db.Column(db.Integer, primary_key=True)
config_content = db.Column(db.Text, nullable=False)
change_type = db.Column(db.String(50)) # vhost_create, vhost_edit, vhost_delete, manual_edit
vhost_id = db.Column(db.Integer, db.ForeignKey('virtual_hosts.id'))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
# Relationships
vhost = db.relationship('VirtualHost')
user = db.relationship('User')
def __repr__(self):
return f'<ConfigHistory {self.change_type} at {self.created_at}>'