rewrite
This commit is contained in:
123
utils/cert_manager.py
Normal file
123
utils/cert_manager.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Certificate Management Utilities - Parse, Validate, Save"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_certificate(pem_content):
|
||||
"""
|
||||
Parse PEM certificate and extract metadata
|
||||
Returns: dict with 'common_name', 'expires_at', 'subject_alt_names', 'error'
|
||||
"""
|
||||
try:
|
||||
# Split cert and key if combined
|
||||
cert_only = None
|
||||
key_only = None
|
||||
|
||||
# Extract certificate
|
||||
cert_match = re.search(
|
||||
r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
|
||||
pem_content,
|
||||
re.DOTALL
|
||||
)
|
||||
if not cert_match:
|
||||
return {'error': 'No valid certificate found in PEM file'}
|
||||
|
||||
cert_only = cert_match.group(0)
|
||||
|
||||
# Extract private key if present
|
||||
key_match = re.search(
|
||||
r'-----BEGIN (?:RSA )?PRIVATE KEY-----.*?-----END (?:RSA )?PRIVATE KEY-----',
|
||||
pem_content,
|
||||
re.DOTALL
|
||||
)
|
||||
if key_match:
|
||||
key_only = key_match.group(0)
|
||||
|
||||
# Parse certificate
|
||||
try:
|
||||
cert = x509.load_pem_x509_certificate(
|
||||
cert_only.encode(),
|
||||
default_backend()
|
||||
)
|
||||
except Exception as e:
|
||||
return {'error': f'Invalid certificate: {str(e)}'}
|
||||
|
||||
# Extract common name
|
||||
common_name = None
|
||||
try:
|
||||
common_name = cert.subject.get_attributes_for_oid(
|
||||
x509.oid.NameOID.COMMON_NAME
|
||||
)[0].value
|
||||
except:
|
||||
pass
|
||||
|
||||
# Extract Subject Alternative Names (SAN)
|
||||
subject_alt_names = []
|
||||
try:
|
||||
san_ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
for name in san_ext.value:
|
||||
if isinstance(name, x509.DNSName):
|
||||
subject_alt_names.append(name.value)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Extract dates
|
||||
issued_at = cert.not_valid_before if cert.not_valid_before else None
|
||||
expires_at = cert.not_valid_after if cert.not_valid_after else None
|
||||
|
||||
result = {
|
||||
'common_name': common_name,
|
||||
'cert_only': cert_only,
|
||||
'key_only': key_only,
|
||||
'issued_at': issued_at,
|
||||
'expires_at': expires_at,
|
||||
'subject_alt_names': subject_alt_names
|
||||
}
|
||||
|
||||
logger.info(f"[CERT] Parsed certificate: CN={common_name}, expires={expires_at}", flush=True)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[CERT] Error parsing certificate: {e}", flush=True)
|
||||
return {'error': f'Failed to parse certificate: {str(e)}'}
|
||||
|
||||
|
||||
def save_cert_file(cert_path, cert_content):
|
||||
"""Save certificate to disk"""
|
||||
try:
|
||||
# Create directory if not exists
|
||||
os.makedirs(os.path.dirname(cert_path), exist_ok=True)
|
||||
|
||||
# Write file
|
||||
with open(cert_path, 'w') as f:
|
||||
f.write(cert_content)
|
||||
|
||||
# Set permissions (owner only can read)
|
||||
os.chmod(cert_path, 0o600)
|
||||
|
||||
logger.info(f"[CERT] Saved certificate file: {cert_path}", flush=True)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[CERT] Error saving certificate file: {e}", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def delete_cert_file(cert_path):
|
||||
"""Delete certificate file from disk"""
|
||||
try:
|
||||
if os.path.exists(cert_path):
|
||||
os.remove(cert_path)
|
||||
logger.info(f"[CERT] Deleted certificate file: {cert_path}", flush=True)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[CERT] Error deleting certificate file: {e}", flush=True)
|
||||
return False
|
||||
222
utils/config_generator.py
Normal file
222
utils/config_generator.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user