62 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
2cc28155fe rewrite 2025-11-04 10:41:27 +01:00
Mateusz Gruszczyński
dd31c1bdd0 rewrite 2025-11-04 10:38:46 +01:00
Mateusz Gruszczyński
762e51f886 rewrite 2025-11-04 10:36:57 +01:00
Mateusz Gruszczyński
84759f9508 rewrite 2025-11-04 10:33:32 +01:00
Mateusz Gruszczyński
5a687549a9 rewrite 2025-11-04 10:31:32 +01:00
Mateusz Gruszczyński
577dc789fc rewrite 2025-11-04 10:30:28 +01:00
Mateusz Gruszczyński
398ccce3b5 rewrite 2025-11-04 10:26:40 +01:00
Mateusz Gruszczyński
411291c8b9 rewrite 2025-11-04 10:22:32 +01:00
Mateusz Gruszczyński
bb3aa9f179 rewrite 2025-11-04 10:19:12 +01:00
Mateusz Gruszczyński
f8a05554c1 rewrite 2025-11-04 10:15:51 +01:00
Mateusz Gruszczyński
f5cfc5bb33 rewrite 2025-11-04 10:12:07 +01:00
Mateusz Gruszczyński
06fce272c1 rewrite 2025-11-04 10:10:14 +01:00
Mateusz Gruszczyński
75e3718e70 rewrite 2025-11-04 10:07:55 +01:00
Mateusz Gruszczyński
899e698353 rewrite 2025-11-04 10:04:58 +01:00
Mateusz Gruszczyński
03b7f20b8c rewrite 2025-11-04 10:03:59 +01:00
Mateusz Gruszczyński
1c6ecb9230 rewrite 2025-11-04 10:02:43 +01:00
Mateusz Gruszczyński
b85efadd87 rewrite 2025-11-04 09:59:56 +01:00
Mateusz Gruszczyński
0c321859b9 rewrite 2025-11-04 09:58:21 +01:00
Mateusz Gruszczyński
addb21bc3e rewrite 2025-11-04 09:56:37 +01:00
gru
32ef62e4ac Merge pull request 'new_functions' (#2) from new_functions into master
Reviewed-on: #2
2025-11-04 09:04:37 +01:00
Mateusz Gruszczyński
04acb4ac21 fixes 2025-11-04 08:59:16 +01:00
Mateusz Gruszczyński
9949e34d68 fixes 2025-11-04 08:54:29 +01:00
Mateusz Gruszczyński
0a027bbebd fixes 2025-11-04 08:51:11 +01:00
Mateusz Gruszczyński
3e7861f489 fixes 2025-11-04 08:47:17 +01:00
Mateusz Gruszczyński
da1af612ef fixes 2025-11-04 08:43:42 +01:00
Mateusz Gruszczyński
370c7099f5 fixes 2025-11-04 08:40:06 +01:00
Mateusz Gruszczyński
27f9984574 fixes 2025-11-04 08:26:41 +01:00
Mateusz Gruszczyński
34c84f1115 fixes 2025-11-04 08:20:39 +01:00
gru
71b0b39a0f Merge pull request 'new_functions_and_fixes' (#1) from new_functions_and_fixes into master
Reviewed-on: #1
2025-11-03 14:35:20 +01:00
Mateusz Gruszczyński
bdc9231ea3 new options 2025-11-03 12:51:01 +01:00
Mateusz Gruszczyński
3763a60adf new options 2025-11-03 12:46:11 +01:00
Mateusz Gruszczyński
c349c8e77a new options 2025-11-03 12:38:11 +01:00
Mateusz Gruszczyński
087d2a46c3 new options 2025-11-03 12:34:08 +01:00
Mateusz Gruszczyński
f2c9f166f6 new options 2025-11-03 12:25:14 +01:00
Mateusz Gruszczyński
c857258dc6 new options 2025-11-03 12:19:21 +01:00
Mateusz Gruszczyński
2d53843f34 new options 2025-11-03 12:17:32 +01:00
Mateusz Gruszczyński
c7a09171e1 new options 2025-11-03 12:08:32 +01:00
Mateusz Gruszczyński
86de6f24bd new options 2025-11-03 12:00:05 +01:00
Mateusz Gruszczyński
7b49105ba3 new options 2025-11-03 11:53:11 +01:00
Mateusz Gruszczyński
f082495a13 new options 2025-11-03 11:43:02 +01:00
Mateusz Gruszczyński
ca39dd35ae new options 2025-11-03 11:20:55 +01:00
Mateusz Gruszczyński
84d7139c15 new options 2025-11-03 11:14:41 +01:00
Mateusz Gruszczyński
8254290049 new options 2025-11-03 11:07:03 +01:00
Mateusz Gruszczyński
ae280b1062 new options 2025-11-03 11:04:01 +01:00
Mateusz Gruszczyński
df1355ec2d new options 2025-11-03 11:02:15 +01:00
Mateusz Gruszczyński
4539f03f9a new options 2025-11-03 10:58:34 +01:00
Mateusz Gruszczyński
82f335020b new options 2025-11-03 10:53:52 +01:00
Mateusz Gruszczyński
45d8634f08 new options 2025-11-03 10:38:40 +01:00
Mateusz Gruszczyński
c838521adc new options 2025-11-03 10:34:18 +01:00
Mateusz Gruszczyński
9fae35fe8a new options 2025-11-03 10:33:23 +01:00
Mateusz Gruszczyński
58205be555 new options 2025-11-03 10:32:38 +01:00
Mateusz Gruszczyński
d01ca3512e new options 2025-11-03 10:27:52 +01:00
Mateusz Gruszczyński
014dc76ff6 new options 2025-11-03 10:22:36 +01:00
Mateusz Gruszczyński
df70118653 new options 2025-11-03 10:18:10 +01:00
Mateusz Gruszczyński
acef7eb610 new options 2025-11-03 09:53:30 +01:00
Mateusz Gruszczyński
7a33291342 new options 2025-11-03 09:52:14 +01:00
Mateusz Gruszczyński
72bf6eb9d1 new options 2025-11-03 09:39:34 +01:00
Mateusz Gruszczyński
e4a3671f90 new options 2025-11-03 09:32:23 +01:00
Mateusz Gruszczyński
8683af493f new options 2025-11-03 09:25:56 +01:00
Mateusz Gruszczyński
80a0d22d4e new options 2025-11-03 08:53:47 +01:00
Mateusz Gruszczyński
f96b426788 new options 2025-11-03 08:42:40 +01:00
Mateusz Gruszczyński
b305368690 new options 2025-11-03 08:30:30 +01:00
48 changed files with 5746 additions and 1030 deletions

View File

@@ -1,9 +1,28 @@
__pycache__
*.pyc
*.pyo
*.egg-info
.git
.gitignore
.env
*.log
.pytest_cache
venv/
.env.local
.vscode
.idea
.pytest_cache
.coverage
htmlcov
dist
build
*.log
venv/
env/
.DS_Store
*.db-journal
*.swp
*.swo
*~
.temp
instance/app.db
uploads/
backups/
logs/

19
.env.example Normal file
View File

@@ -0,0 +1,19 @@
# ===== FLASK =====
FLASK_ENV=production
FLASK_APP=app.py
FLASK_DEBUG=0
# ===== DATABASE =====
DATABASE_URL=sqlite:////app/instance/app.db
# ===== SECURITY =====
SECRET_KEY=your-super-secret-key-change-this
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123
# ===== HAPROXY =====
HAPROXY_CONFIG_PATH=/etc/haproxy/haproxy.cfg
HAPROXY_STATS_PORT=8404
# ===== LOGGING =====
LOG_LEVEL=INFO

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
venv
logs/*
__pycache__
config/*
config/auth/*

View File

@@ -1,49 +1,80 @@
FROM python:3.14-rc-trixie
# ===== ENV VARIABLES =====
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1
# Install dependencies
RUN apt-get update && apt-get install -y \
LABEL maintainer="HAProxy Manager" \
version="2.0" \
description="HAProxy Configuration Manager with SQLAlchemy"
# ===== INSTALL SYSTEM DEPENDENCIES =====
RUN apt-get update && apt-get install -y --no-install-recommends \
haproxy \
supervisor \
openssl \
ca-certificates \
supervisor \
curl \
&& rm -rf /var/lib/apt/lists/*
wget \
git \
vim \
libssl-dev \
libffi-dev \
python3-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# ===== WORKDIR =====
WORKDIR /app
# Copy requirements and install
# ===== COPY & INSTALL PYTHON REQUIREMENTS =====
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt
# Copy application
# ===== COPY APPLICATION FILES =====
COPY app.py .
COPY log_parser.py .
# Copy directory structure
COPY config/ config/
COPY database/ database/
COPY routes/ routes/
COPY utils/ utils/
COPY auth/ auth/
COPY templates/ templates/
COPY static/ /app/static/
COPY static/ static/
# Create directories
RUN mkdir -p /app/config/auth \
&& mkdir -p /app/config/ssl \
# ===== CREATE REQUIRED DIRECTORIES =====
RUN mkdir -p /app/instance \
&& mkdir -p /app/uploads/certificates \
&& mkdir -p /app/backups \
&& mkdir -p /app/logs \
&& mkdir -p /etc/haproxy \
&& mkdir -p /etc/supervisor/conf.d \
&& mkdir -p /var/log/supervisor \
&& mkdir -p /etc/haproxy
&& mkdir -p /var/run/haproxy \
&& touch /etc/haproxy/haproxy.cfg
# Copy configs
# ===== COPY CONFIGS =====
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f https://localhost:5000 --insecure 2>/dev/null || exit 1
# ===== SET PERMISSIONS (POPRAWIONE - && ВЕЗДЕ!) =====
RUN chmod +x /entrypoint.sh && \
chmod 755 /app && \
chmod -R 755 /app/uploads && \
chmod -R 755 /app/backups && \
chmod -R 755 /app/logs
# ===== EXPOSE PORTS =====
EXPOSE 5000 80 443 8404
# ===== HEALTHCHECK =====
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/api/current-user 2>/dev/null || exit 1
# ===== ENTRYPOINT =====
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,22 @@
# 1. Przygotuj environment
cp .env.example .env
# Edytuj .env - zmień SECRET_KEY i hasła!
# 2. Build images
docker-compose build
# 3. Start services
docker-compose up -d
# 4. Sprawdź logi
docker-compose logs -f haproxy-configurator
# 5. Dostęp
# Web UI: http://localhost:15001/login
# HAProxy Stats: http://localhost:8405/stats
# HAProxy HTTP: http://localhost:81
# HAProxy HTTPS: https://localhost:444 (with warning)
# 6. Zaloguj się
# Username: admin
# Password: admin123 (zmień potem!)

295
app.py
View File

@@ -1,121 +1,252 @@
"""
HAProxy Configurator - Main Application
SQLAlchemy + Flask-SQLAlchemy Integration
"""
from flask import Flask, render_template, render_template_string
import configparser
import ssl
from routes.main_routes import main_bp
from routes.edit_routes import edit_bp
from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats
from auth.auth_middleware import setup_auth
from log_parser import parse_log_file
import os
import sys
from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends
import ssl
import configparser
from datetime import timedelta
from flask import Flask, render_template, redirect, url_for, session
from flask_sqlalchemy import SQLAlchemy
from config.settings import *
from database import db, migrate, init_db
from routes.main_routes import main_bp
from routes.edit_routes import edit_bp
from routes.auth_routes import auth_bp
from routes.user_routes import user_bp
from routes.vhost_routes import vhost_bp
from routes.cert_routes import cert_bp
from auth.auth_middleware import setup_auth, login_required
from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats
# ===== BASE DIRECTORY =====
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
# ===== CREATE REQUIRED DIRECTORIES =====
INSTANCE_DIR = os.path.join(BASE_DIR, 'instance')
os.makedirs(INSTANCE_DIR, exist_ok=True)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(HAPROXY_BACKUP_DIR, exist_ok=True)
print(f"[APP] Base directory: {BASE_DIR}", flush=True)
print(f"[APP] Instance directory: {INSTANCE_DIR}", flush=True)
print(f"[APP] Database: {SQLALCHEMY_DATABASE_URI}", flush=True)
# ===== CREATE FLASK APP =====
app = Flask(
__name__,
static_folder=os.path.join(BASE_DIR, 'static'),
static_url_path='/static',
template_folder=os.path.join(BASE_DIR, 'templates')
template_folder=os.path.join(BASE_DIR, 'templates'),
instance_path=INSTANCE_DIR
)
CONFIG_DIR_DOCKER = '/etc/haproxy-configurator'
CONFIG_DIR_LOCAL = './config'
CONFIG_DIR_ENV = os.environ.get('CONFIG_DIR', None)
if CONFIG_DIR_ENV and os.path.exists(CONFIG_DIR_ENV):
CONFIG_DIR = CONFIG_DIR_ENV
elif os.path.exists(CONFIG_DIR_DOCKER):
CONFIG_DIR = CONFIG_DIR_DOCKER
elif os.path.exists(CONFIG_DIR_LOCAL):
CONFIG_DIR = CONFIG_DIR_LOCAL
else:
CONFIG_DIR = CONFIG_DIR_DOCKER
# ===== LOAD CONFIGURATION =====
app.config.from_object('config.settings')
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
app.config['SESSION_COOKIE_SECURE'] = False
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
AUTH_CFG = os.path.join(CONFIG_DIR, 'auth', 'auth.cfg')
SSL_INI = os.path.join(CONFIG_DIR, 'ssl.ini')
os.makedirs(os.path.dirname(AUTH_CFG), exist_ok=True)
os.makedirs(os.path.dirname(SSL_INI), exist_ok=True)
# ===== INITIALIZE DATABASE =====
print("[APP] Initializing database...", flush=True)
db.init_app(app)
migrate.init_app(app, db)
print("[APP] Database initialized", flush=True)
BASIC_AUTH_USERNAME = "admin"
BASIC_AUTH_PASSWORD = "admin"
try:
auth_config = configparser.ConfigParser()
auth_config.read(AUTH_CFG)
if auth_config.has_section('auth'):
BASIC_AUTH_USERNAME = auth_config.get('auth', 'username', fallback='admin')
BASIC_AUTH_PASSWORD = auth_config.get('auth', 'password', fallback='admin')
else:
BASIC_AUTH_USERNAME = "admin"
BASIC_AUTH_PASSWORD = "admin"
except Exception as e:
print(f"[APP] Auth config error: {e}, using defaults", flush=True)
BASIC_AUTH_USERNAME = "admin"
BASIC_AUTH_PASSWORD = "admin"
# ===== REGISTER BLUEPRINTS =====
print("[APP] Registering blueprints...", flush=True)
app.register_blueprint(main_bp)
app.register_blueprint(edit_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(user_bp)
app.register_blueprint(vhost_bp)
app.register_blueprint(cert_bp)
print("[APP] Blueprints registered", flush=True)
# ===== SETUP AUTHENTICATION MIDDLEWARE =====
print("[APP] Setting up auth middleware...", flush=True)
setup_auth(app)
print("[APP] Auth middleware setup complete", flush=True)
# ===== SSL CONTEXT SETUP =====
certificate_path = None
private_key_path = None
ssl_context = None
try:
config2 = configparser.ConfigParser()
config2.read(SSL_INI)
if config2.has_section('ssl'):
certificate_path = config2.get('ssl', 'certificate_path')
private_key_path = config2.get('ssl', 'private_key_path')
if os.path.exists(SSL_INI):
config_ssl = configparser.ConfigParser()
config_ssl.read(SSL_INI)
if config_ssl.has_section('ssl'):
certificate_path = config_ssl.get('ssl', 'certificate_path')
private_key_path = config_ssl.get('ssl', 'private_key_path')
if os.path.exists(certificate_path) and os.path.exists(private_key_path):
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path)
print("[APP] SSL context loaded successfully", flush=True)
else:
print(f"[APP] SSL certificate files not found", flush=True)
print(f" Certificate: {certificate_path}", flush=True)
print(f" Private Key: {private_key_path}", flush=True)
else:
print(f"[APP] No [ssl] section in {SSL_INI}", flush=True)
else:
print(f"[APP] No [ssl] section in {SSL_INI}", flush=True)
sys.exit(1)
if not os.path.exists(certificate_path):
print(f"[APP] ✗ Certificate not found: {certificate_path}", flush=True)
sys.exit(1)
if not os.path.exists(private_key_path):
print(f"[APP] ✗ Private key not found: {private_key_path}", flush=True)
sys.exit(1)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path)
print(f"[APP] ✓ SSL context loaded", flush=True)
print(f"[APP] No SSL config file found: {SSL_INI}", flush=True)
except Exception as e:
print(f"[APP] SSL error: {e}", flush=True)
sys.exit(1)
print(f"[APP] SSL warning (non-critical): {e}", flush=True)
# ===== ROUTES =====
@app.route('/')
def index():
"""Index/Home - Redirect to login or dashboard"""
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return redirect(url_for('main.index'))
@app.route('/statistics')
@login_required
def display_haproxy_stats():
haproxy_stats = fetch_haproxy_stats()
parsed_stats = parse_haproxy_stats(haproxy_stats)
return render_template('statistics.html', stats=parsed_stats)
@app.route('/logs')
def display_logs():
log_file_path = '/var/log/haproxy.log'
parsed_entries = parse_log_file(log_file_path)
return render_template('logs.html', entries=parsed_entries)
"""Display HAProxy statistics"""
try:
haproxy_stats = fetch_haproxy_stats()
parsed_stats = parse_haproxy_stats(haproxy_stats)
return render_template('statistics.html', stats=parsed_stats)
except Exception as e:
print(f"[STATS] Error: {e}", flush=True)
return render_template('statistics.html', stats={}, error=str(e))
@app.route('/home')
def home():
frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends()
return render_template('home.html',
frontend_count=frontend_count,
backend_count=backend_count,
acl_count=acl_count,
layer7_count=layer7_count,
layer4_count=layer4_count)
@app.route('/dashboard')
@login_required
def dashboard():
"""Dashboard - Overview of vhosts and status"""
from database.models import VirtualHost
try:
vhosts = VirtualHost.query.all()
return render_template('dashboard.html', vhosts=vhosts)
except Exception as e:
print(f"[DASHBOARD] Error: {e}", flush=True)
return render_template('dashboard.html', vhosts=[], error=str(e))
# ===== ERROR HANDLERS =====
@app.errorhandler(404)
def page_not_found(error):
"""404 error handler"""
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
"""500 error handler"""
db.session.rollback()
return render_template('500.html'), 500
@app.errorhandler(403)
def forbidden(error):
"""403 error handler"""
return render_template('403.html'), 403
# ===== SHELL CONTEXT =====
@app.shell_context_processor
def make_shell_context():
"""Add models to Flask shell context"""
from database.models import User, Certificate, VirtualHost, BackendServer, ConfigHistory
return {
'db': db,
'User': User,
'Certificate': Certificate,
'VirtualHost': VirtualHost,
'BackendServer': BackendServer,
'ConfigHistory': ConfigHistory
}
@app.before_request
def before_request():
"""Run before each request"""
pass
@app.after_request
def after_request(response):
"""Run after each request"""
return response
# ===== CLI COMMANDS =====
@app.cli.command()
def init_db_cli():
"""Initialize database"""
init_db(app)
print("[CLI] Database initialized successfully", flush=True)
@app.cli.command()
def create_admin():
"""Create admin user"""
import getpass
username = input("Username: ")
password = getpass.getpass("Password: ")
from database.models import User
if User.query.filter_by(username=username).first():
print(f"Error: User '{username}' already exists")
return
user = User(username=username, is_admin=True)
user.set_password(password)
db.session.add(user)
db.session.commit()
print(f"[CLI] Admin user '{username}' created successfully")
# ===== MAIN ENTRY POINT =====
if __name__ == '__main__':
app.run(host='::', port=5000, ssl_context=ssl_context, debug=True)
print("""
╔════════════════════════════════════════╗
║ HAProxy Configurator & Manager ║
║ Starting Application... ║
╚════════════════════════════════════════╝
""", flush=True)
print(f"[APP] Environment: {'Development' if DEBUG else 'Production'}", flush=True)
print(f"[APP] Running on: https://[::]:5000 (IPv6)", flush=True)
# Initialize database before running
init_db(app)
app.run(
host='::',
port=5000,
ssl_context=None,
debug=DEBUG,
use_reloader=DEBUG
)

View File

@@ -1,53 +1,39 @@
import os
import functools
from flask import request, Response
import configparser
"""Auth middleware - Updated for database"""
# Docker paths
CONFIG_DIR = './config'
AUTH_CFG = os.path.join(CONFIG_DIR, 'auth', 'auth.cfg')
from functools import wraps
from flask import session, redirect, url_for
from database.models import User
# Ensure config directory exists
os.makedirs(os.path.dirname(AUTH_CFG), exist_ok=True)
# Load auth credentials with fallback defaults
BASIC_AUTH_USERNAME = "admin"
BASIC_AUTH_PASSWORD = "admin"
try:
if os.path.exists(AUTH_CFG):
auth_config = configparser.ConfigParser()
auth_config.read(AUTH_CFG)
if auth_config.has_section('auth'):
BASIC_AUTH_USERNAME = auth_config.get('auth', 'username', fallback='admin')
BASIC_AUTH_PASSWORD = auth_config.get('auth', 'password', fallback='admin')
print(f"[AUTH] Loaded credentials from {AUTH_CFG}", flush=True)
else:
print(f"[AUTH] No [auth] section in {AUTH_CFG}, using defaults", flush=True)
else:
print(f"[AUTH] {AUTH_CFG} not found, using defaults", flush=True)
except Exception as e:
print(f"[AUTH] Error loading config: {e}, using defaults", flush=True)
def check_auth(username, password):
return username == BASIC_AUTH_USERNAME and password == BASIC_AUTH_PASSWORD
def authenticate():
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials',
401,
{'WWW-Authenticate': 'Basic realm="Login Required"'}
)
def requires_auth(f):
@functools.wraps(f)
def login_required(f):
"""Require login for view"""
@wraps(f)
def decorated_function(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
if 'user_id' not in session:
return redirect(url_for('auth.login', next=request.url))
# Verify user still exists
user = User.query.get(session['user_id'])
if not user:
session.clear()
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
def setup_auth(app):
pass
"""Setup auth for Flask app"""
@app.before_request
def before_request():
"""Before each request - update session user info"""
if 'user_id' in session:
user = User.query.get(session['user_id'])
if user:
# Sync session data
session['username'] = user.username
session['is_admin'] = user.is_admin
else:
# User was deleted
session.clear()

1
config/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Application configuration module"""

48
config/settings.py Normal file
View File

@@ -0,0 +1,48 @@
"""Application Settings"""
import os
from datetime import timedelta
# ===== ENVIRONMENT =====
DEBUG = os.getenv('FLASK_DEBUG', 'False').lower() == 'true'
ENV = os.getenv('FLASK_ENV', 'production')
# ===== BASE PATHS =====
BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
INSTANCE_DIR = os.path.join(BASE_DIR, 'instance')
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads/certificates')
HAPROXY_BACKUP_DIR = os.path.join(BASE_DIR, 'backups')
# ===== DATABASE =====
SQLALCHEMY_DATABASE_URI = os.getenv(
'DATABASE_URL',
f'sqlite:///{os.path.join(INSTANCE_DIR, "app.db")}'
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# ===== FLASK SETTINGS =====
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# ===== HAPROXY =====
HAPROXY_CONFIG_PATH = os.getenv('HAPROXY_CONFIG_PATH', '/etc/haproxy/haproxy.cfg')
HAPROXY_BACKUP_DIR = os.path.join(BASE_DIR, 'backups')
HAPROXY_STATS_PORT = int(os.getenv('HAPROXY_STATS_PORT', '8404'))
# ===== SSL =====
SSL_INI = os.path.join(BASE_DIR, 'config', 'ssl.ini')
# ===== MAX UPLOAD SIZE =====
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
# ===== LOGGING =====
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
LOG_FILE = os.path.join(BASE_DIR, 'logs', 'app.log')
os.makedirs(INSTANCE_DIR, exist_ok=True)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(HAPROXY_BACKUP_DIR, exist_ok=True)
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)

45
database/__init__.py Normal file
View File

@@ -0,0 +1,45 @@
"""Database module initialization"""
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import logging
db = SQLAlchemy()
migrate = Migrate()
logger = logging.getLogger(__name__)
def init_db(app):
"""Initialize database - create tables"""
with app.app_context():
try:
db.create_all()
print("[DB] All tables created successfully", flush=True)
except Exception as e:
print(f"[DB] Error creating tables: {e}", flush=True)
raise
from database.models import User
try:
admin = User.query.filter_by(username='admin').first()
if not admin:
print("[DB] Creating default admin user...", flush=True)
admin = User(username='admin', is_admin=True)
admin.set_password('admin123')
db.session.add(admin)
db.session.commit()
print("[DB] Default admin user created (admin/admin123)", flush=True)
print(f"[DB] Hash: {admin.password_hash}", flush=True)
else:
print("[DB] Admin user already exists", flush=True)
except Exception as e:
print(f"[DB] Error creating admin: {e}", flush=True)
db.session.rollback()
raise

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/models.py 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}>'

View File

@@ -1,40 +1,115 @@
version: '3.9'
services:
haproxy-configurator:
haproxy-configurator-app:
build:
context: .
dockerfile: Dockerfile
container_name: haproxy-configurator
container_name: haproxy-configurator-app
restart: unless-stopped
# ===== PORTS =====
ports:
- "15000:5000"
- "80:80"
- "443:443"
- "8404:8404"
- "15001:5000" # Flask app (manager UI)
- "81:80" # HAProxy HTTP
- "444:443" # HAProxy HTTPS
- "8405:8404" # HAProxy Stats (hardcoded 8404 inside)
# ===== VOLUMES =====
volumes:
- ./config:/app/config
- ./haproxy:/etc/haproxy
- ./logs:/var/log
- ./ssl:/app/ssl
# Application data
- ./instance:/app/instance # SQLite database
- ./uploads/certificates:/app/uploads/certificates # SSL certificates
- ./backups:/app/backups # Config backups
# HAProxy config
- ./haproxy:/etc/haproxy # HAProxy config directory
- ./logs/haproxy:/var/log/haproxy # HAProxy logs
# Logs
- ./logs/app:/app/logs # Application logs
- ./logs/supervisor:/var/log/supervisor # Supervisor logs
# ===== ENVIRONMENT =====
environment:
# Flask
- FLASK_ENV=production
- FLASK_APP=app.py
- FLASK_DEBUG=0
- PYTHONUNBUFFERED=1
- PYTHONDONTWRITEBYTECODE=1
# Database
- DATABASE_URL=sqlite:////app/instance/app.db
# Admin credentials (initial)
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=admin123
# Secret key (CHANGE IN PRODUCTION!)
- SECRET_KEY=change-me-in-production-$(openssl rand -hex 16)
# HAProxy
- HAPROXY_CONFIG_PATH=/etc/haproxy/haproxy.cfg
- HAPROXY_STATS_PORT=8404
# ===== CAPABILITIES =====
cap_add:
- NET_ADMIN
- SYS_ADMIN
- DAC_OVERRIDE
# ===== LOGGING =====
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels: "service=haproxy-configurator"
networks: [intranet]
# ===== HEALTHCHECK =====
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/current-user", "--fail"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# ===== NETWORK =====
networks:
- intranet
# ===== DEPENDENCIES =====
depends_on:
- haproxy-configurator-init
# ===== INIT SERVICE (tworzy tabele i strukturę) =====
haproxy-configurator-init:
build:
context: .
dockerfile: Dockerfile
container_name: haproxy-configurator-init
entrypoint: >
sh -c "
echo '[INIT] Initializing database...';
python -c 'from app import app; from database import init_db; init_db(app)';
echo '[INIT] Database initialized!';
exit 0;
"
volumes:
- ./instance:/app/instance
- ./uploads/certificates:/app/uploads/certificates
- ./backups:/app/backups
environment:
- FLASK_ENV=production
- FLASK_APP=app.py
- PYTHONUNBUFFERED=1
networks:
- intranet
restart: "no"
networks:
intranet:
external: true
driver: bridge
# uncomment dla external network:
# external: true

View File

@@ -1,77 +1,41 @@
#!/bin/bash
set -e
echo "[$(date)] Starting HAProxy Configurator..."
echo "╔════════════════════════════════════════╗"
echo "║ HAProxy Manager - Entrypoint ║"
echo "║ Starting services... ║"
echo "╚════════════════════════════════════════╝"
# Create directories if they don't exist
mkdir -p /app/config/auth
mkdir -p /app/config/ssl
mkdir -p /etc/haproxy
mkdir -p /var/log/supervisor
# ===== CHECK ENVIRONMENT =====
echo "[STARTUP] Environment: ${FLASK_ENV:-production}"
echo "[STARTUP] Python version: $(python --version)"
# Create default auth.cfg if doesn't exist
if [ ! -f /app/config/auth/auth.cfg ]; then
cat > /app/config/auth/auth.cfg <<EOF
[auth]
username = admin
password = admin123
EOF
echo "[$(date)] Created default auth.cfg"
fi
# ===== INIT DATABASE =====
echo "[STARTUP] Initializing database..."
python -c "
from app import app
from database import init_db
with app.app_context():
init_db(app)
print('[STARTUP] Database initialized successfully')
" || {
echo "[ERROR] Database initialization failed!"
exit 1
}
# Create default ssl.ini if doesn't exist
if [ ! -f /app/config/ssl.ini ]; then
cat > /app/config/ssl.ini <<EOF
[ssl]
certificate_path = /app/config/ssl/haproxy-configurator.pem
private_key_path = /app/config/ssl/haproxy-configurator.pem
EOF
echo "[$(date)] Created default ssl.ini"
fi
# ===== GENERATE INITIAL HAPROXY CONFIG =====
echo "[STARTUP] Generating initial HAProxy config..."
python -c "
from app import app
from utils.config_generator import generate_haproxy_config, save_haproxy_config
with app.app_context():
config = generate_haproxy_config()
save_haproxy_config(config)
print('[STARTUP] HAProxy config generated')
" || {
echo "[WARNING] Could not generate initial config, continuing..."
}
# Generate self-signed certificate if doesn't exist
if [ ! -f /app/config/ssl/haproxy-configurator.pem ]; then
openssl req -x509 -newkey rsa:2048 -keyout /app/config/ssl/haproxy-configurator.pem \
-out /app/config/ssl/haproxy-configurator.pem -days 365 -nodes \
-subj "/C=PL/ST=State/L=City/O=Organization/CN=haproxy-configurator.local"
chmod 600 /app/config/ssl/haproxy-configurator.pem
echo "[$(date)] Generated SSL certificate"
fi
# Create default haproxy.cfg if doesn't exist or is empty
if [ ! -s /etc/haproxy/haproxy.cfg ]; then
cat > /etc/haproxy/haproxy.cfg <<'HAPROXYCFG'
global
log stdout local0
maxconn 4096
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats show-legends
HAPROXYCFG
echo "[$(date)] Created default haproxy.cfg"
fi
# Set proper permissions
chmod 600 /app/config/ssl/haproxy-configurator.pem 2>/dev/null || true
chmod 644 /app/config/auth/auth.cfg
chmod 644 /app/config/ssl.ini
chmod 644 /etc/haproxy/haproxy.cfg
echo "[$(date)] Configuration ready"
echo "[$(date)] Starting supervisord..."
# Start supervisord
# ===== START SUPERVISOR (runs Flask + HAProxy + other services) =====
echo "[STARTUP] Starting supervisord..."
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

View File

@@ -1,7 +1,14 @@
import re
def parse_log_file(log_file_path):
"""
Parse HAProxy syslog format and identify security threats.
Format: <134>Nov 3 09:18:35 haproxy[18]: IP:PORT [DATE:TIME] FRONTEND BACKEND STATUS BYTES ...
"""
parsed_entries = []
# Security threat patterns
xss_patterns = [
r'<\s*script\s*',
r'javascript:',
@@ -12,92 +19,147 @@ def parse_log_file(log_file_path):
r'<\s*input\s*[^>]*\s*value\s*=?',
r'<\s*form\s*action\s*=?',
r'<\s*svg\s*on\w+\s*=?',
r'script',
r'alert',
r'alert\s*\(',
r'onerror',
r'onload',
r'javascript'
]
sql_patterns = [
r';',
r'substring',
r'extract',
r'union\s+all',
r'order\s+by',
r'(union|select|insert|update|delete|drop)\s+(from|into|table)',
r';\s*(union|select|insert|update|delete|drop)',
r'substring\s*\(',
r'extract\s*\(',
r'order\s+by\s+\d+',
r'--\+',
r'union',
r'select',
r'insert',
r'update',
r'delete',
r'drop',
r'@@',
r'1=1',
r'1\s*=\s*1',
r'@@\w+',
r'`1',
r'union',
r'select',
r'insert',
r'update',
r'delete',
r'drop',
r'@@',
r'1=1',
r'`1'
]
webshells_patterns = [
r'payload',
r'eval|system|passthru|shell_exec|exec|popen|proc_open|pcntl_exec|cmd|shell|backdoor|webshell|phpspy|c99|kacak|b374k|log4j|log4shell|wsos|madspot|malicious|evil.*\.php.*'
r'eval\s*\(',
r'system\s*\(',
r'passthru\s*\(',
r'shell_exec\s*\(',
r'exec\s*\(',
r'popen\s*\(',
r'proc_open\s*\(',
r'backdoor|webshell|phpspy|c99|kacak|b374k|wsos',
]
combined_xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE)
combined_sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE)
combined_webshells_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE)
with open(log_file_path, 'r') as log_file:
log_lines = log_file.readlines()
# Compile patterns
xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE)
sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE)
webshell_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE)
try:
with open(log_file_path, 'r', encoding='utf-8', errors='ignore') as log_file:
log_lines = log_file.readlines()
for line in log_lines:
if " 403 " in line: # Check if the line contains " 403 " indicating a 403 status code
match = re.search(r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"\s*(GET|POST|PUT|DELETE)\s+([^"]+)"', line)
if match:
timestamp = match.group(1) # Extract the date and time
ip_address = match.group(2)
http_method = match.group(3)
requested_url = match.group(4)
if not line.strip():
continue
if combined_xss_pattern.search(line):
xss_alert = 'Possible XSS Attack Was Identified.'
try:
# Extract syslog header
syslog_match = re.search(
r'<\d+>(\w+\s+\d+\s+\d+:\d+:\d+).*haproxy\[\d+\]:\s+',
line
)
if not syslog_match:
continue
timestamp = syslog_match.group(1)
# Extract IP:PORT
ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', line)
if not ip_match:
continue
ip_address = ip_match.group(1)
# Extract date/time in brackets (preferred format)
datetime_match = re.search(r'\[(\d{2}/\w+/\d{4}:\d{2}:\d{2}:\d{2})', line)
if datetime_match:
timestamp = datetime_match.group(1)
# Extract frontend and backend
fe_be_match = re.search(r'\]\s+(\S+)\s+(\S+)\s+(\d+/\d+/\d+/\d+/\d+)\s+(\d{3})', line)
if not fe_be_match:
continue
frontend = fe_be_match.group(1)
backend = fe_be_match.group(2)
status_code = fe_be_match.group(4)
# Extract HTTP method and URL
http_match = re.search(r'"(\w+)\s+([^\s]+)\s+HTTP', line)
if not http_match:
# Fallback: extract entire request line
request_match = re.search(r'"([^"]*)"', line)
if request_match:
request_line = request_match.group(1).split()
http_method = request_line[0] if len(request_line) > 0 else 'UNKNOWN'
requested_url = request_line[1] if len(request_line) > 1 else '/'
else:
xss_alert = ''
if combined_sql_pattern.search(line):
sql_alert = 'Possible SQL Injection Attempt Was Made.'
else:
sql_alert = ''
if "PUT" in line:
put_method = 'Possible Remote File Upload Attempt Was Made.'
else:
put_method = ''
if "admin" in line:
illegal_resource = 'Possible Illegal Resource Access Attempt Was Made.'
else:
illegal_resource = ''
if combined_webshells_pattern.search(line):
webshell_alert = 'Possible WebShell Attack Attempt Was Made.'
else:
webshell_alert = ''
parsed_entries.append({
'timestamp': timestamp,
'ip_address': ip_address,
'http_method': http_method,
'requested_url': requested_url,
'xss_alert': xss_alert,
'sql_alert': sql_alert,
'put_method': put_method,
'illegal_resource': illegal_resource,
'webshell_alert': webshell_alert
})
return parsed_entries
continue
else:
http_method = http_match.group(1)
requested_url = http_match.group(2)
# Detect threats
xss_alert = bool(xss_pattern.search(line))
sql_alert = bool(sql_pattern.search(line))
webshell_alert = bool(webshell_pattern.search(line))
put_method = http_method == 'PUT'
illegal_resource = status_code == '403'
# Determine status class for UI coloring
status_class = 'secondary'
if status_code.startswith('2'):
status_class = 'success'
elif status_code.startswith('3'):
status_class = 'info'
elif status_code.startswith('4'):
status_class = 'warning'
if illegal_resource:
status_class = 'warning'
elif status_code.startswith('5'):
status_class = 'danger'
# Add threat flag if any security issue detected
has_threat = xss_alert or sql_alert or webshell_alert or put_method or illegal_resource
if has_threat:
status_class = 'danger'
parsed_entries.append({
'timestamp': timestamp,
'ip_address': ip_address,
'http_method': http_method,
'requested_url': requested_url,
'status_code': status_code,
'frontend': frontend,
'backend': backend,
'xss_alert': xss_alert,
'sql_alert': sql_alert,
'put_method': put_method,
'illegal_resource': illegal_resource,
'webshell_alert': webshell_alert,
'status_class': status_class,
'has_threat': has_threat,
'message': f"{frontend}~ {backend} [{status_code}] {http_method} {requested_url}"
})
except Exception as e:
print(f"[LOG_PARSER] Error parsing line: {e}", flush=True)
continue
except FileNotFoundError:
print(f"[LOG_PARSER] Log file not found: {log_file_path}", flush=True)
return []
except Exception as e:
print(f"[LOG_PARSER] Error reading log file: {e}", flush=True)
return []
print(f"[LOG_PARSER] Parsed {len(parsed_entries)} log entries", flush=True)
return parsed_entries

178
models.py Normal file
View File

@@ -0,0 +1,178 @@
"""Database Models - HAProxy Manager"""
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from config.settings import HAPROXY_STATS_PORT
import json
db = SQLAlchemy()
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}>'
# ===== CONSTANTS (dla config_generator.py) =====
# HAPROXY_STATS_PORT importujemy z config.settings
# Default: 8404 (hardcoded w config/settings.py)

View File

@@ -1,4 +1,11 @@
flask
Flask
Flask-SQLAlchemy
Flask-Migrate
Werkzeug
python-dateutil
cryptography
pyopenssl
requests
pyOpenSSL
Werkzeug
Jinja2
Markupsafe
pyOpenSSL

122
routes/auth_routes.py Normal file
View File

@@ -0,0 +1,122 @@
"""Authentication routes - Login, Logout"""
from flask import Blueprint, render_template, request, redirect, url_for, session, jsonify
from functools import wraps
from database import db
from database.models import User
import logging
auth_bp = Blueprint('auth', __name__)
logger = logging.getLogger(__name__)
def login_required(f):
"""Decorator - require user to be logged in"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""Decorator - require admin role"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
user = User.query.get(session['user_id'])
if not user or not user.is_admin:
return jsonify({'error': 'Admin access required', 'success': False}), 403
return f(*args, **kwargs)
return decorated_function
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""Login page and authentication"""
if request.method == 'GET':
# Check if already logged in
if 'user_id' in session:
return redirect(url_for('main.index'))
return render_template('login.html')
# POST - process login
username = request.form.get('username', '').strip()
password = request.form.get('password', '').strip()
if not username or not password:
return render_template('login.html', error='Username and password required'), 400
try:
# Find user
user = User.query.filter_by(username=username).first()
if not user:
logger.warning(f"[AUTH] Login failed - user '{username}' not found", flush=True)
return render_template('login.html', error='Invalid credentials'), 401
# Check password
if not user.check_password(password):
logger.warning(f"[AUTH] Login failed - wrong password for '{username}'", flush=True)
return render_template('login.html', error='Invalid credentials'), 401
session.clear()
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
session.permanent = True
# Zaloguj w basie danych
from datetime import datetime
user.last_login = datetime.utcnow()
db.session.commit()
logger.info(f"[AUTH] User '{username}' logged in successfully", flush=True)
# Redirect do dashboard
return redirect(url_for('main.index'))
except Exception as e:
logger.error(f"[AUTH] Login error: {e}", flush=True)
return render_template('login.html', error='Login error'), 500
@auth_bp.route('/logout', methods=['GET', 'POST'])
def logout():
"""Logout"""
username = session.get('username', 'unknown')
session.clear()
logger.info(f"[AUTH] User '{username}' logged out", flush=True)
return redirect(url_for('auth.login'))
@auth_bp.route('/api/current-user', methods=['GET'])
def current_user():
"""Get current logged in user info"""
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated', 'success': False}), 401
try:
user = User.query.get(session['user_id'])
if not user:
session.clear()
return jsonify({'error': 'User not found', 'success': False}), 401
return jsonify({
'success': True,
'user': {
'id': user.id,
'username': user.username,
'is_admin': user.is_admin
}
})
except Exception as e:
logger.error(f"[AUTH] Error getting current user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500

217
routes/cert_routes.py Normal file
View File

@@ -0,0 +1,217 @@
"""Certificate Management - Upload, List, Delete"""
from flask import Blueprint, request, jsonify, session
from functools import wraps
from database import db
from database.models import Certificate, VirtualHost
from utils.cert_manager import parse_certificate, save_cert_file, delete_cert_file
from config.settings import UPLOAD_FOLDER, MAX_CONTENT_LENGTH
from datetime import datetime
import os
import logging
cert_bp = Blueprint('certs', __name__, url_prefix='/api/certificates')
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
@cert_bp.route('', methods=['GET'])
@login_required_api
def list_certificates():
"""Get all certificates"""
try:
certs = Certificate.query.order_by(Certificate.created_at.desc()).all()
return jsonify({
'success': True,
'certificates': [{
'id': c.id,
'name': c.name,
'common_name': c.common_name,
'expires_at': c.expires_at.isoformat() if c.expires_at else None,
'created_at': c.created_at.isoformat(),
'vhost_count': len(c.vhosts),
'is_expired': c.expires_at < datetime.utcnow() if c.expires_at else False
} for c in certs]
})
except Exception as e:
logger.error(f"[CERTS] Error listing: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('/<int:cert_id>', methods=['GET'])
@login_required_api
def get_certificate(cert_id):
"""Get certificate details"""
try:
cert = Certificate.query.get(cert_id)
if not cert:
return jsonify({'error': 'Certificate not found', 'success': False}), 404
return jsonify({
'success': True,
'certificate': {
'id': cert.id,
'name': cert.name,
'common_name': cert.common_name,
'subject_alt_names': cert.get_san_list(),
'issued_at': cert.issued_at.isoformat() if cert.issued_at else None,
'expires_at': cert.expires_at.isoformat() if cert.expires_at else None,
'created_at': cert.created_at.isoformat(),
'vhosts': [{
'id': v.id,
'name': v.name,
'hostname': v.hostname
} for v in cert.vhosts]
}
})
except Exception as e:
logger.error(f"[CERTS] Error getting cert {cert_id}: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('', methods=['POST'])
@login_required_api
def upload_certificate():
"""Upload SSL certificate (PEM format)"""
try:
# Check if file provided
if 'cert_file' not in request.files:
return jsonify({'error': 'No certificate file provided', 'success': False}), 400
file = request.files['cert_file']
cert_name = request.form.get('name', '').strip()
if not file or file.filename == '':
return jsonify({'error': 'No selected file', 'success': False}), 400
if not cert_name:
return jsonify({'error': 'Certificate name required', 'success': False}), 400
# Check if name already exists
if Certificate.query.filter_by(name=cert_name).first():
return jsonify({'error': 'Certificate name already exists', 'success': False}), 400
# Read file content
cert_content = file.read().decode('utf-8')
# Validate and parse certificate
cert_data = parse_certificate(cert_content)
if 'error' in cert_data:
return jsonify({'error': cert_data['error'], 'success': False}), 400
# Create certificate record
cert = Certificate(
name=cert_name,
cert_content=cert_content,
cert_only=cert_data.get('cert_only'),
key_only=cert_data.get('key_only'),
common_name=cert_data.get('common_name'),
issued_at=cert_data.get('issued_at'),
expires_at=cert_data.get('expires_at')
)
if cert_data.get('subject_alt_names'):
cert.set_san_list(cert_data['subject_alt_names'])
db.session.add(cert)
db.session.flush()
# Save cert file to disk
cert_path = os.path.join(UPLOAD_FOLDER, f'{cert_name}.pem')
if not save_cert_file(cert_path, cert_content):
db.session.rollback()
return jsonify({'error': 'Failed to save certificate file', 'success': False}), 500
db.session.commit()
logger.info(f"[CERTS] Uploaded certificate '{cert_name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'id': cert.id,
'name': cert.name,
'message': 'Certificate uploaded successfully'
}), 201
except Exception as e:
db.session.rollback()
logger.error(f"[CERTS] Error uploading cert: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('/<int:cert_id>', methods=['DELETE'])
@login_required_api
def delete_certificate(cert_id):
"""Delete certificate"""
try:
cert = Certificate.query.get(cert_id)
if not cert:
return jsonify({'error': 'Certificate not found', 'success': False}), 404
# Check if certificate is in use
if cert.vhosts:
vhost_names = [v.name for v in cert.vhosts]
return jsonify({
'error': f'Certificate is in use by vhosts: {", ".join(vhost_names)}',
'success': False
}), 400
cert_name = cert.name
# Delete file from disk
cert_path = os.path.join(UPLOAD_FOLDER, f'{cert_name}.pem')
delete_cert_file(cert_path)
# Delete from database
db.session.delete(cert)
db.session.commit()
logger.info(f"[CERTS] Deleted certificate '{cert_name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': f'Certificate {cert_name} deleted successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[CERTS] Error deleting cert: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('/<int:cert_id>/export', methods=['GET'])
@login_required_api
def export_certificate(cert_id):
"""Download certificate in PEM format"""
try:
cert = Certificate.query.get(cert_id)
if not cert:
return jsonify({'error': 'Certificate not found', 'success': False}), 404
# Read from disk
cert_path = os.path.join(UPLOAD_FOLDER, f'{cert.name}.pem')
if not os.path.exists(cert_path):
return jsonify({'error': 'Certificate file not found', 'success': False}), 404
with open(cert_path, 'r') as f:
cert_content = f.read()
return jsonify({
'success': True,
'name': cert.name,
'content': cert_content
})
except Exception as e:
logger.error(f"[CERTS] Error exporting cert: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500

View File

@@ -1,91 +1,21 @@
from flask import Blueprint, render_template, request
import subprocess
from auth.auth_middleware import requires_auth
"""Edit HAProxy configuration"""
edit_bp = Blueprint('edit', __name__)
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for
from routes.auth_routes import login_required
from database.models import VirtualHost
import logging
@edit_bp.route('/edit', methods=['GET', 'POST'])
@requires_auth
edit_bp = Blueprint('edit', __name__, url_prefix='/edit')
logger = logging.getLogger(__name__)
@edit_bp.route('/')
@login_required
def edit_haproxy_config():
if request.method == 'POST':
edited_config = request.form['haproxy_config']
try:
with open('/etc/haproxy/haproxy.cfg', 'w') as f:
f.write(edited_config)
except Exception as e:
return render_template(
'edit.html',
config_content=edited_config,
check_output=f"Error writing configuration: {e}",
check_level="danger"
)
def run_check():
result = subprocess.run(
['haproxy', '-c', '-V', '-f', '/etc/haproxy/haproxy.cfg'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
out = (result.stdout or '').strip()
if result.returncode == 0:
if not out:
out = "Configuration file is valid ✅"
level = "success"
if "Warning" in out or "Warnings" in out:
level = "warning"
else:
if not out:
out = f"Check failed with return code {result.returncode}"
level = "danger"
return result.returncode, out, level
check_output = ""
check_level = "success"
if 'save_check' in request.form:
_, check_output, check_level = run_check()
elif 'save_reload' in request.form:
rc, out, level = run_check()
check_output, check_level = out, level
if rc == 0:
try:
supervisor_result = subprocess.run(
['pkill', '-f', 'haproxy'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=10
)
if supervisor_result.returncode == 0:
check_output += f"\n\nHAProxy Restarted:\n{supervisor_result.stdout}"
else:
check_output += (
f"\n\nRestart attempt returned {supervisor_result.returncode}:\n"
f"{supervisor_result.stdout}"
)
except Exception as e:
check_output += f"\n\nRestart failed: {e}"
check_level = "warning"
return render_template(
'edit.html',
config_content=edited_config,
check_output=check_output,
check_level=check_level
)
"""Edit configuration page"""
try:
with open('/etc/haproxy/haproxy.cfg', 'r') as f:
config_content = f.read()
except FileNotFoundError:
config_content = "# HAProxy configuration file not found\n# Please create /etc/haproxy/haproxy.cfg"
except PermissionError:
config_content = "# Permission denied reading HAProxy configuration file"
return render_template('edit.html', config_content=config_content)
vhosts = VirtualHost.query.all()
return render_template('edit.html', vhosts=vhosts)
except Exception as e:
logger.error(f"[EDIT] Error: {e}", flush=True)
return render_template('edit.html', vhosts=[], error=str(e))

View File

@@ -1,104 +1,39 @@
from flask import Blueprint, render_template, request
from auth.auth_middleware import requires_auth # Updated import
from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends
"""Main routes - Dashboard, Home"""
from flask import Blueprint, render_template, redirect, url_for, session
from database.models import VirtualHost
from routes.auth_routes import login_required
import logging
main_bp = Blueprint('main', __name__)
logger = logging.getLogger(__name__)
@main_bp.route('/', methods=['GET', 'POST'])
@requires_auth
@main_bp.route('/')
def index():
if request.method == 'POST':
frontend_name = request.form['frontend_name']
frontend_ip = request.form['frontend_ip']
frontend_port = request.form['frontend_port']
lb_method = request.form['lb_method']
protocol = request.form['protocol']
backend_name = request.form['backend_name']
add_header = 'add_header' in request.form
header_name = request.form.get('header_name', '') if add_header else ''
header_value = request.form.get('header_value', '') if add_header else ''
"""Dashboard - list vhosts"""
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return render_template('dashboard.html')
# Get all backend servers data
backend_server_names = request.form.getlist('backend_server_names[]')
backend_server_ips = request.form.getlist('backend_server_ips[]')
backend_server_ports = request.form.getlist('backend_server_ports[]')
backend_server_maxconns = request.form.getlist('backend_server_maxconns[]')
is_acl = 'add_acl' in request.form
acl_name = request.form['acl'] if 'acl' in request.form else ''
acl_action = request.form['acl_action'] if 'acl_action' in request.form else ''
acl_backend_name = request.form['backend_name_acl'] if 'backend_name_acl' in request.form else ''
use_ssl = 'ssl_checkbox' in request.form
ssl_cert_path = request.form['ssl_cert_path']
https_redirect = 'ssl_redirect_checkbox' in request.form
is_dos = 'add_dos' in request.form if 'add_dos' in request.form else ''
ban_duration = request.form["ban_duration"]
limit_requests = request.form["limit_requests"]
forward_for = 'forward_for_check' in request.form
@main_bp.route('/home')
@login_required
def home():
"""Home - alias for dashboard"""
return redirect(url_for('main.index'))
is_forbidden_path = 'add_acl_path' in request.form
forbidden_name = request.form["forbidden_name"]
allowed_ip = request.form["allowed_ip"]
forbidden_path = request.form["forbidden_path"]
sql_injection_check = 'sql_injection_check' in request.form if 'sql_injection_check' in request.form else ''
is_xss = 'xss_check' in request.form if 'xss_check' in request.form else ''
is_remote_upload = 'remote_uploads_check' in request.form if 'remote_uploads_check' in request.form else ''
@main_bp.route('/display_logs')
@login_required
def display_logs():
"""Display HAProxy logs"""
return render_template('logs.html')
add_path_based = 'add_path_based' in request.form
redirect_domain_name = request.form["redirect_domain_name"]
root_redirect = request.form["root_redirect"]
redirect_to = request.form["redirect_to"]
is_webshells = 'webshells_check' in request.form if 'webshells_check' in request.form else ''
# Combine backend server info into a list of tuples (name, ip, port, maxconns)
backend_servers = []
for i in range(len(backend_server_ips)):
name = backend_server_names[i] if i < len(backend_server_names) else f"server{i+1}"
ip = backend_server_ips[i] if i < len(backend_server_ips) else ''
port = backend_server_ports[i] if i < len(backend_server_ports) else ''
maxconn = backend_server_maxconns[i] if i < len(backend_server_maxconns) else None
if ip and port: # Only add if we have IP and port
backend_servers.append((name, ip, port, maxconn))
# Check if frontend or port already exists
if is_frontend_exist(frontend_name, frontend_ip, frontend_port):
return render_template('index.html', message="Frontend or Port already exists. Cannot add duplicate.")
# Get health check related fields if the protocol is HTTP
health_check = False
health_check_link = ""
if protocol == 'http':
health_check = 'health_check' in request.form
if health_check:
health_check_link = request.form['health_check_link']
health_check_tcp = False
if protocol == 'tcp':
health_check_tcp = 'health_check2' in request.form
# Get sticky session related fields
sticky_session = False
sticky_session_type = ""
if 'sticky_session' in request.form:
sticky_session = True
sticky_session_type = request.form['sticky_session_type']
# Update the HAProxy config file
message = update_haproxy_config(
frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name,
backend_servers, health_check, health_check_tcp, health_check_link, sticky_session,
add_header, header_name, header_value, sticky_session_type, is_acl, acl_name,
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells
)
return render_template('index.html', message=message)
return render_template('index.html')
@main_bp.route('/display_haproxy_stats')
@login_required
def display_haproxy_stats():
"""Display HAProxy statistics"""
return render_template('statistics.html')

211
routes/user_routes.py Normal file
View File

@@ -0,0 +1,211 @@
"""User management routes - Admin only"""
from flask import Blueprint, request, jsonify, session
from functools import wraps
from database import db
from database.models import User
from routes.auth_routes import admin_required
import logging
user_bp = Blueprint('users', __name__, url_prefix='/api/users')
logger = logging.getLogger(__name__)
def api_admin_required(f):
"""Decorator for API admin requirement"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated'}), 401
user = User.query.get(session['user_id'])
if not user or not user.is_admin:
return jsonify({'error': 'Admin access required'}), 403
return f(*args, **kwargs)
return decorated_function
@user_bp.route('', methods=['GET'])
@api_admin_required
def list_users():
"""List all users"""
try:
users = User.query.all()
return jsonify({
'success': True,
'users': [{
'id': u.id,
'username': u.username,
'is_admin': u.is_admin,
'created_at': u.created_at.isoformat(),
'last_login': u.last_login.isoformat() if u.last_login else None
} for u in users]
})
except Exception as e:
logger.error(f"[USERS] Error listing users: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('', methods=['POST'])
@api_admin_required
def create_user():
"""Create new user"""
try:
data = request.json
username = data.get('username', '').strip()
password = data.get('password', '').strip()
is_admin = data.get('is_admin', False)
if not username or not password:
return jsonify({'error': 'Username and password required', 'success': False}), 400
if len(username) < 3:
return jsonify({'error': 'Username must be at least 3 characters', 'success': False}), 400
if len(password) < 6:
return jsonify({'error': 'Password must be at least 6 characters', 'success': False}), 400
# Check if exists
if User.query.filter_by(username=username).first():
return jsonify({'error': 'Username already exists', 'success': False}), 400
user = User(username=username, is_admin=is_admin)
user.set_password(password)
db.session.add(user)
db.session.commit()
logger.info(f"[USERS] Created user '{username}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'id': user.id,
'username': user.username,
'is_admin': user.is_admin,
'message': 'User created successfully'
}), 201
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error creating user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('/<int:user_id>', methods=['PUT'])
@api_admin_required
def update_user(user_id):
"""Update user"""
try:
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found', 'success': False}), 404
data = request.json
# Don't allow self-demotion
if user.id == session['user_id'] and 'is_admin' in data and not data['is_admin']:
return jsonify({'error': 'Cannot remove admin role from yourself', 'success': False}), 400
if 'is_admin' in data:
user.is_admin = data['is_admin']
if 'password' in data and data['password']:
password = data['password'].strip()
if len(password) < 6:
return jsonify({'error': 'Password must be at least 6 characters', 'success': False}), 400
user.set_password(password)
db.session.commit()
logger.info(f"[USERS] Updated user '{user.username}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': 'User updated successfully',
'id': user.id,
'username': user.username,
'is_admin': user.is_admin
})
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error updating user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('/<int:user_id>', methods=['DELETE'])
@api_admin_required
def delete_user(user_id):
"""Delete user"""
try:
# Don't allow self-deletion
if user_id == session['user_id']:
return jsonify({'error': 'Cannot delete your own account', 'success': False}), 400
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found', 'success': False}), 404
username = user.username
db.session.delete(user)
db.session.commit()
logger.info(f"[USERS] Deleted user '{username}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': f'User {username} deleted successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error deleting user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('/<int:user_id>/change-password', methods=['POST'])
def change_password(user_id):
"""Change own password"""
try:
# Check if logged in
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated', 'success': False}), 401
# Can only change own password, or admin can change others
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found', 'success': False}), 404
is_admin = session.get('is_admin', False)
is_own_account = session['user_id'] == user_id
if not is_own_account and not is_admin:
return jsonify({'error': 'Forbidden', 'success': False}), 403
data = request.json
# If changing own password, require old password
if is_own_account:
old_password = data.get('old_password', '').strip()
if not user.check_password(old_password):
return jsonify({'error': 'Current password is incorrect', 'success': False}), 400
new_password = data.get('new_password', '').strip()
if len(new_password) < 6:
return jsonify({'error': 'Password must be at least 6 characters', 'success': False}), 400
user.set_password(new_password)
db.session.commit()
logger.info(f"[USERS] Password changed for '{user.username}'", flush=True)
return jsonify({
'success': True,
'message': 'Password changed successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error changing password: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500

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

View File

@@ -1,3 +1,5 @@
#!/bin/bash
git pull
docker compose down
docker compose up --build
docker compose up --remove-orphans --build --no-deps --force-recreate

48
static/css/edit.css Normal file
View File

@@ -0,0 +1,48 @@
.CodeMirror {
height: 500px !important;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 13px;
border: none;
}
.CodeMirror-gutters {
background-color: #263238;
border-right: 1px solid #37474f;
}
.CodeMirror-linenumber {
color: #546e7a;
}
.CodeMirror-cursor {
border-left: 1px solid #fff;
}
#haproxy_config {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 13px;
line-height: 1.5;
padding: 12px;
border: none;
width: 100%;
overflow: hidden;
resize: none;
background: #1e1e1e;
color: #e8e8e8;
}
#edit_form button {
white-space: nowrap;
}
@media (max-width: 768px) {
.CodeMirror {
height: 300px !important;
font-size: 12px;
}
#haproxy_config {
font-size: 12px;
min-height: 300px;
}
}

219
static/js/cert_manager.js Normal file
View File

@@ -0,0 +1,219 @@
/**
* Certificate Manager - Upload, List, Delete
*/
let currentCertId = null;
document.addEventListener('DOMContentLoaded', function() {
loadCertificates();
document.getElementById('uploadBtn').addEventListener('click', uploadCertificate);
document.getElementById('exportBtn').addEventListener('click', exportCertificate);
});
function loadCertificates() {
fetch('/api/certificates')
.then(r => r.json())
.then(data => {
if (data.success) {
renderCertificates(data.certificates);
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => console.error('Error loading certificates:', e));
}
function renderCertificates(certs) {
const tbody = document.getElementById('certsList');
if (certs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No certificates uploaded yet</td></tr>';
return;
}
tbody.innerHTML = certs.map(c => {
const expiresDate = c.expires_at ? new Date(c.expires_at) : null;
const isExpired = c.is_expired;
const expiresText = expiresDate ? formatDate(expiresDate) : 'Unknown';
return `
<tr ${isExpired ? 'class="table-danger"' : ''}>
<td><strong>${escapeHtml(c.name)}</strong></td>
<td><code>${escapeHtml(c.common_name || 'N/A')}</code></td>
<td>
${isExpired ? '<span class="badge bg-danger">EXPIRED</span>' : ''}
<small>${expiresText}</small>
</td>
<td>
${c.vhost_count > 0
? `<span class="badge bg-info">${c.vhost_count}</span>`
: '<span class="text-muted">Not used</span>'}
</td>
<td>
${isExpired
? '<span class="badge bg-danger">Expired</span>'
: '<span class="badge bg-success">Valid</span>'}
</td>
<td>
<button class="btn btn-sm btn-info" onclick="viewDetails(${c.id})">
<i class="bi bi-eye"></i> View
</button>
${c.vhost_count === 0 ? `
<button class="btn btn-sm btn-danger" onclick="deleteCert(${c.id}, '${escapeHtml(c.name)}')">
<i class="bi bi-trash"></i>
</button>
` : ''}
</td>
</tr>
`;
}).join('');
}
function uploadCertificate() {
const name = document.getElementById('cert_name').value;
const file = document.getElementById('cert_file').files[0];
if (!name || !file) {
showAlert('Certificate name and file are required', 'warning');
return;
}
const formData = new FormData();
formData.append('name', name);
formData.append('cert_file', file);
fetch('/api/certificates', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide();
document.getElementById('uploadForm').reset();
loadCertificates();
showAlert('Certificate uploaded successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function viewDetails(certId) {
currentCertId = certId;
fetch(`/api/certificates/${certId}`)
.then(r => r.json())
.then(data => {
if (data.success) {
const c = data.certificate;
document.getElementById('detailsName').textContent = c.name;
const san = c.subject_alt_names && c.subject_alt_names.length > 0
? c.subject_alt_names.join(', ')
: 'No SAN';
const vhosts = c.vhosts && c.vhosts.length > 0
? c.vhosts.map(v => `<li>${v.name} (${v.hostname})</li>`).join('')
: '<li class="text-muted">Not used</li>';
const detailsHTML = `
<table class="table table-sm">
<tr>
<th>Common Name</th>
<td><code>${escapeHtml(c.common_name || 'N/A')}</code></td>
</tr>
<tr>
<th>Subject Alt Names</th>
<td><code>${escapeHtml(san)}</code></td>
</tr>
<tr>
<th>Issued</th>
<td>${c.issued_at ? formatDate(new Date(c.issued_at)) : 'Unknown'}</td>
</tr>
<tr>
<th>Expires</th>
<td>
${c.expires_at ? formatDate(new Date(c.expires_at)) : 'Unknown'}
${new Date(c.expires_at) < new Date() ? '<span class="badge bg-danger ms-2">EXPIRED</span>' : ''}
</td>
</tr>
<tr>
<th>Created</th>
<td>${formatDate(new Date(c.created_at))}</td>
</tr>
</table>
<h6>Used by VHosts:</h6>
<ul>${vhosts}</ul>
`;
document.getElementById('detailsContent').innerHTML = detailsHTML;
new bootstrap.Modal(document.getElementById('detailsModal')).show();
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function exportCertificate() {
fetch(`/api/certificates/${currentCertId}/export`)
.then(r => r.json())
.then(data => {
if (data.success) {
// Create download link
const blob = new Blob([data.content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${data.name}.pem`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function deleteCert(certId, name) {
if (!confirm(`Delete certificate '${name}'? This cannot be undone.`)) return;
fetch(`/api/certificates/${certId}`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
if (data.success) {
loadCertificates();
showAlert('Certificate deleted successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.card').parentElement.insertBefore(alertDiv, document.querySelector('.card'));
setTimeout(() => alertDiv.remove(), 5000);
}
function formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
function escapeHtml(text) {
const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return text.replace(/[&<>"']/g, m => map[m]);
}

133
static/js/editor.js Normal file
View File

@@ -0,0 +1,133 @@
/**
* HAProxy Configuration Editor
* Auto-grow textarea + CodeMirror integration
*/
document.addEventListener('DOMContentLoaded', function() {
// Auto-grow textarea (fallback if CodeMirror fails)
initAutoGrowTextarea();
// Try to initialize CodeMirror
initCodeMirror();
});
/**
* Initialize auto-grow textarea
*/
function initAutoGrowTextarea() {
'use strict';
const ta = document.getElementById('haproxy_config');
if (!ta) return;
const autoGrow = () => {
ta.style.height = 'auto';
ta.style.height = (ta.scrollHeight + 6) + 'px';
};
ta.addEventListener('input', autoGrow);
ta.addEventListener('change', autoGrow);
// Initial auto-size
autoGrow();
// Resize on window resize
window.addEventListener('resize', autoGrow);
console.log('[Editor] Auto-grow textarea initialized');
}
/**
* Initialize CodeMirror editor
*/
function initCodeMirror() {
'use strict';
// Check if CodeMirror is available
if (typeof CodeMirror === 'undefined') {
console.warn('[Editor] CodeMirror not loaded, using fallback textarea');
document.getElementById('haproxy_config').style.display = 'block';
return;
}
try {
const editorElement = document.getElementById('haproxy_editor');
if (!editorElement) {
console.warn('[Editor] haproxy_editor element not found');
return;
}
const editor = CodeMirror.fromTextArea(editorElement, {
lineNumbers: true,
lineWrapping: true,
indentUnit: 4,
indentWithTabs: false,
theme: 'material-darker',
mode: 'text/x-nginx-conf',
styleActiveLine: true,
styleSelectedText: true,
highlightSelectionMatches: { annotateScrollbar: true },
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
matchBrackets: true,
autoCloseBrackets: true,
extraKeys: {
'Ctrl-S': function() {
document.querySelector('button[value="save"]').click();
},
'Ctrl-L': function() {
editor.clearHistory();
},
'Ctrl-/': 'toggleComment'
}
});
// Hide fallback textarea
document.getElementById('haproxy_config').style.display = 'none';
// Update line/col info
editor.on('cursorActivity', function() {
const pos = editor.getCursor();
document.getElementById('line_col').textContent =
`Line ${pos.line + 1}, Col ${pos.ch + 1}`;
document.getElementById('char_count').textContent =
editor.getValue().length;
});
// Auto-save to localStorage
let saveTimeout;
editor.on('change', function() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
localStorage.setItem('haproxy_draft', editor.getValue());
}, 1000);
});
// Recover from localStorage
const draft = localStorage.getItem('haproxy_draft');
const currentContent = editorElement.value.trim();
if (draft && draft.trim() !== currentContent && currentContent === '') {
if (confirm('📝 Recover unsaved draft?')) {
editor.setValue(draft);
localStorage.removeItem('haproxy_draft');
}
}
// Form submission - sync values
const editForm = document.getElementById('edit_form');
editForm.addEventListener('submit', function(e) {
editorElement.value = editor.getValue();
document.getElementById('haproxy_config').value = editor.getValue();
});
// Initial info
document.getElementById('char_count').textContent = editor.getValue().length;
console.log('[Editor] CodeMirror initialized successfully');
} catch (e) {
console.warn('[Editor] CodeMirror initialization failed:', e);
// Fallback textarea is already visible
document.getElementById('haproxy_config').style.display = 'block';
}
}

137
static/js/form.js Normal file
View File

@@ -0,0 +1,137 @@
(() => {
'use strict';
// Helper functions (shared)
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// ===== HEALTH CHECK FIELDS (Protocol-dependent) =====
const protocolSelect = document.getElementById('protocol');
const healthCheckFields = document.getElementById('health_check_fields');
const tcpHealthCheck = document.getElementById('tcp_health_check');
const onProtocolChange = () => {
if (protocolSelect?.value === 'http') {
const healthCheckParent = document.getElementById('health_check')?.parentElement.parentElement;
if (healthCheckParent) healthCheckParent.style.display = 'block';
if (tcpHealthCheck) tcpHealthCheck.style.display = 'none';
} else {
const healthCheckParent = document.getElementById('health_check')?.parentElement.parentElement;
if (healthCheckParent) healthCheckParent.style.display = 'none';
if (tcpHealthCheck) tcpHealthCheck.style.display = 'flex';
}
};
protocolSelect?.addEventListener('change', onProtocolChange);
// ===== STICKY SESSION FIELDS =====
const stickyCheckbox = document.getElementById('sticky_session');
const stickyFields = document.getElementById('sticky_fields');
stickyCheckbox?.addEventListener('change', function() {
stickyFields?.classList.toggle('d-none', !this.checked);
});
// ===== HEALTH CHECK LINK FIELD =====
const healthCheckbox = document.getElementById('health_check');
healthCheckbox?.addEventListener('change', function() {
document.getElementById('health_check_fields')?.classList.toggle('d-none', !this.checked);
});
// ===== CUSTOM HEADER FIELDS =====
const headerCheckbox = document.getElementById('add_header');
const headerFields = document.querySelectorAll('#header_fields');
headerCheckbox?.addEventListener('change', function() {
headerFields.forEach(field => field.classList.toggle('d-none', !this.checked));
});
// ===== NO-LB MODE HANDLING =====
const lbMethodSelect = $('#lb_method');
const backendServersContainer = $('#backend_servers_container');
const addServerBtn = $('#add_backend_btn');
const onLbMethodChange = () => {
const isNoLb = lbMethodSelect?.value === 'no-lb';
if (isNoLb) {
// Hide add server button
if (addServerBtn) addServerBtn.classList.add('d-none');
// Keep only first server and remove others
const serverRows = $$('.backend-server-row', backendServersContainer);
serverRows.forEach((row, idx) => {
if (idx > 0) row.remove();
});
// Add info about no-lb mode if it doesn't exist
if (!$('.no-lb-info')) {
const info = document.createElement('div');
info.className = 'alert alert-info alert-sm no-lb-info mt-2';
info.innerHTML = '<i class="bi bi-info-circle me-2"></i><small>Mode <strong>no-lb</strong>: frontend → backend → single server. You can still enable XSS, DOS, SQL injection protection etc.</small>';
if (backendServersContainer?.parentElement) {
backendServersContainer.parentElement.appendChild(info);
}
}
} else {
// Show add server button
if (addServerBtn) addServerBtn.classList.remove('d-none');
// Remove no-lb info
const info = $('.no-lb-info');
if (info) info.remove();
}
};
lbMethodSelect?.addEventListener('change', onLbMethodChange);
if (lbMethodSelect) onLbMethodChange();
// ===== BACKEND SERVER ROWS (Dynamic Add/Remove) =====
let serverCount = 1;
const container = $('#backend_servers_container');
const addBtn = $('#add_backend_btn');
const createRow = () => {
serverCount++;
const row = document.createElement('div');
row.className = 'row g-3 backend-server-row mt-1';
row.innerHTML = `
<div class="col-md-3">
<input type="text" class="form-control" name="backend_server_names[]" placeholder="server${serverCount}" required>
</div>
<div class="col-md-4">
<input type="text" class="form-control" name="backend_server_ips[]" placeholder="192.168.1.${serverCount}" required>
</div>
<div class="col-md-2">
<input type="number" class="form-control" name="backend_server_ports[]" placeholder="80" min="1" max="65535" required>
</div>
<div class="col-md-2">
<input type="number" class="form-control" name="backend_server_maxconns[]" placeholder="100">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm w-100 remove-server">
<i class="bi bi-trash"></i>
</button>
</div>
`;
const removeBtn = row.querySelector('.remove-server');
removeBtn.addEventListener('click', () => row.remove());
return row;
};
addBtn?.addEventListener('click', () => {
if (container) {
container.appendChild(createRow());
}
});
// Remove button for dynamically added rows
container?.addEventListener('click', (e) => {
if (e.target.closest('.remove-server')) {
e.target.closest('.backend-server-row').remove();
}
});
})();

View File

@@ -1,105 +1,82 @@
(() => {
'use strict';
const $ = (sel, root=document) => root.querySelector(sel);
const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
// SSL fields
const sslCheckbox = $('#ssl_checkbox');
const sslFields = $('#ssl_fields');
const toggle = (on, el) => el.classList.toggle('d-none', !on);
sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields));
// DOS
const dosCheckbox = $('#add_dos');
const dosFields = $('#dos_fields');
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
// HTTP only groups
const protocolSelect = $('#protocol');
const httpGroups = $$('.http-only, #forbidden_acl_container');
const httpToggles = [
$('#sql_injection_check'),
$('#xss_check'),
$('#remote_uploads_check'),
$('#webshells_check'),
$('#forward_for_check'),
$('#add_acl_path'),
$('#add_path_based'),
];
const forbiddenFields = $('#forbidden_fields');
const pathFields = $('#base_redirect_fields');
const onProtocolChange = () => {
const isHttp = protocolSelect?.value === 'http';
httpGroups.forEach(el => toggle(isHttp, el));
if (!isHttp) {
// hide optional groups if protocol != http
[forbiddenFields, pathFields].forEach(el => el && toggle(false, el));
httpToggles.forEach(input => { if (input) input.checked = false; });
}
};
protocolSelect?.addEventListener('change', onProtocolChange);
onProtocolChange();
// ACL
const aclCheckbox = $('#add_acl');
const aclFields = $('#acl_fields');
aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields));
// toggles that reveal their fields
const bindToggle = (checkboxSel, targetSel) => {
const cb = $(checkboxSel);
const target = $(targetSel);
cb?.addEventListener('change', () => toggle(cb.checked, target));
// initial
if (cb && target) toggle(cb.checked, target);
};
bindToggle('#add_path_based', '#base_redirect_fields');
bindToggle('#add_acl_path', '#forbidden_fields');
// Backend rows
let serverCount = 1;
const container = $('#backend_servers_container');
const addBtn = $('#add_backend_btn');
const createRow = () => {
serverCount++;
const row = document.createElement('div');
row.className = 'row g-3 backend-server-row mt-1';
row.innerHTML = `
<div class="col-md-3">
<label class="form-label" for="name${serverCount}">Nazwa serwera</label>
<input type="text" id="name${serverCount}" class="form-control" name="backend_server_names[]" placeholder="server${serverCount}" required>
</div>
<div class="col-md-3">
<label class="form-label" for="ip${serverCount}">IP</label>
<input type="text" id="ip${serverCount}" class="form-control" name="backend_server_ips[]" required>
</div>
<div class="col-md-3">
<label class="form-label" for="port${serverCount}">Port</label>
<input type="number" id="port${serverCount}" class="form-control" name="backend_server_ports[]" required>
</div>
<div class="col-md-3">
<label class="form-label" for="maxconn${serverCount}">MaxConn</label>
<div class="d-flex gap-2">
<input type="number" id="maxconn${serverCount}" class="form-control" name="backend_server_maxconns[]">
<button type="button" class="btn btn-danger" title="Usuń">Usuń</button>
</div>
</div>`;
row.querySelector('button.btn-danger')?.addEventListener('click', () => {
const rows = $$('.backend-server-row');
if (rows.length > 1) row.remove();
else alert('Musi istnieć co najmniej jeden backend.');
'use strict';
// ===== HELPER FUNCTIONS =====
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const toggle = (on, el) => el && el.classList.toggle('d-none', !on);
// ===== SSL FIELDS =====
const sslCheckbox = $('#ssl_checkbox');
const sslFields = $('#ssl_fields');
sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields));
// ===== DOS PROTECTION =====
const dosCheckbox = $('#add_dos');
const dosFields = $('#dos_fields');
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
// ===== PROTOCOL CHANGE (HTTP/TCP) =====
const protocolSelect = $('#protocol');
const httpGroups = $$('.http-only, #forbidden_acl_container');
const httpToggles = [
$('#sql_injection_check'),
$('#xss_check'),
$('#remote_uploads_check'),
$('#webshells_check'),
$('#forward_for_check'),
$('#add_acl_path'),
$('#add_path_based'),
$('#add_custom_acl'),
];
const forbiddenFields = $('#forbidden_fields');
const pathFields = $('#base_redirect_fields');
const onProtocolChange = () => {
const isHttp = protocolSelect?.value === 'http';
httpGroups.forEach(el => toggle(isHttp, el));
if (!isHttp) {
[forbiddenFields, pathFields].forEach(el => toggle(false, el));
httpToggles.forEach(input => {
if (input) input.checked = false;
});
}
};
protocolSelect?.addEventListener('change', onProtocolChange);
onProtocolChange();
// ===== BACKEND SSL REDIRECT =====
const backendSslCheckbox = $('#backend_ssl_redirect');
const backendSslFields = $('#backend_ssl_fields');
backendSslCheckbox?.addEventListener('change', function() {
toggle(this.checked, backendSslFields);
});
return row;
};
addBtn?.addEventListener('click', () => container?.appendChild(createRow()));
// auto dismiss alerts
setTimeout(() => $$('.alert').forEach(a => {
if (typeof bootstrap !== 'undefined') new bootstrap.Alert(a).close();
}), 5000);
// ===== CUSTOM ACL (Main Toggle) =====
const customAclCheckbox = $('#add_custom_acl');
const customAclFields = $('#custom_acl_fields');
customAclCheckbox?.addEventListener('change', function() {
toggle(this.checked, customAclFields);
});
// ===== CUSTOM ACL Action Type Toggle =====
const customAclAction = $('#custom_acl_action');
const aclBackendSelect = $('#acl_backend_select');
const aclRedirectSelect = $('#acl_redirect_select');
const onCustomAclActionChange = () => {
const action = customAclAction?.value;
toggle(action === 'route', aclBackendSelect);
toggle(action === 'redirect', aclRedirectSelect);
};
customAclAction?.addEventListener('change', onCustomAclActionChange);
// Initial state
onCustomAclActionChange();
})();

View File

@@ -0,0 +1,269 @@
/**
* HAProxy Logs Management with Security Alerts
* Fixed pagination
*/
document.addEventListener('DOMContentLoaded', function() {
let currentPage = 1;
let perPage = 50;
let totalLogs = parseInt(document.getElementById('total_count').textContent);
let allLoadedLogs = [];
let excludePhrases = [];
const logsContainer = document.getElementById('logs_container');
const searchFilter = document.getElementById('search_filter');
const excludeFilter = document.getElementById('exclude_filter');
const excludeBtn = document.getElementById('exclude_btn');
const perPageSelect = document.getElementById('logs_per_page');
const refreshBtn = document.getElementById('refresh_logs_btn');
const prevBtn = document.getElementById('prev_btn');
const nextBtn = document.getElementById('next_btn');
const loadAllBtn = document.getElementById('load_all_btn');
const clearFilterBtn = document.getElementById('clear_filter_btn');
const loadedSpan = document.getElementById('loaded_count');
const matchSpan = document.getElementById('match_count');
const currentPageSpan = document.getElementById('current_page');
const totalPagesSpan = document.getElementById('total_pages');
// Event Listeners
searchFilter.addEventListener('keyup', debounce(function() {
console.log('[Logs] Search changed');
currentPage = 1;
loadLogsWithPage();
}, 300));
excludeBtn.addEventListener('click', function() {
const phrase = excludeFilter.value.trim();
if (phrase) {
if (!excludePhrases.includes(phrase)) {
excludePhrases.push(phrase);
updateExcludeUI();
currentPage = 1;
loadLogsWithPage();
}
excludeFilter.value = '';
}
});
excludeFilter.addEventListener('keypress', function(e) {
if (e.key === 'Enter') excludeBtn.click();
});
clearFilterBtn.addEventListener('click', function() {
console.log('[Logs] Clear filters');
searchFilter.value = '';
excludePhrases = [];
excludeFilter.value = '';
updateExcludeUI();
currentPage = 1;
loadLogsWithPage();
});
perPageSelect.addEventListener('change', function() {
console.log(`[Logs] Per page changed to ${this.value}`);
perPage = parseInt(this.value);
currentPage = 1;
loadLogsWithPage();
});
refreshBtn.addEventListener('click', function() {
console.log('[Logs] Refresh clicked');
searchFilter.value = '';
excludePhrases = [];
excludeFilter.value = '';
updateExcludeUI();
currentPage = 1;
loadLogsWithPage();
});
prevBtn.addEventListener('click', function() {
if (currentPage > 1) {
console.log(`[Logs] Prev button: page ${currentPage} -> ${currentPage - 1}`);
currentPage--;
loadLogsWithPage();
}
});
nextBtn.addEventListener('click', function() {
const totalPages = parseInt(document.getElementById('total_pages').textContent);
if (currentPage < totalPages) {
console.log(`[Logs] Next button: page ${currentPage} -> ${currentPage + 1}`);
currentPage++;
loadLogsWithPage();
}
});
loadAllBtn.addEventListener('click', function() {
console.log('[Logs] Load all clicked');
perPage = totalLogs > 500 ? 500 : totalLogs;
currentPage = 1;
perPageSelect.value = perPage;
loadLogsWithPage();
});
/**
* Debounce function
*/
function debounce(func, wait) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
}
/**
* Load logs with pagination from API
*/
function loadLogsWithPage() {
console.log(`[Logs] loadLogsWithPage: page=${currentPage}, per_page=${perPage}, search="${searchFilter.value.trim()}", exclude=${excludePhrases.length}`);
logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4">Loading logs...</td></tr>';
fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
page: currentPage,
per_page: perPage,
search: searchFilter.value.trim(),
exclude: excludePhrases
})
})
.then(r => r.json())
.then(data => {
console.log('[Logs] API Response:', data);
if (data.success) {
allLoadedLogs = data.logs;
loadedSpan.textContent = data.loaded_count;
totalLogs = data.total;
document.getElementById('total_count').textContent = data.total;
const totalPages = Math.ceil(data.total_filtered / perPage) || 1;
totalPagesSpan.textContent = totalPages;
matchSpan.textContent = data.total_filtered;
currentPageSpan.textContent = data.page;
renderLogs(data.logs);
// Update button states
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = !data.has_more;
console.log(`[Logs] Updated: page ${data.page}/${totalPages}, has_more=${data.has_more}, prev_disabled=${prevBtn.disabled}, next_disabled=${nextBtn.disabled}`);
} else {
showError(data.error);
}
})
.catch(e => {
console.error('[Logs] Error:', e);
showError('Failed to load logs: ' + e.message);
});
}
/**
* Render logs as table rows
*/
function renderLogs(logs) {
if (!logs || logs.length === 0) {
logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4">No logs found</td></tr>';
return;
}
logsContainer.innerHTML = logs.map((entry) => {
const threat_badges = [];
if (entry.xss_alert) threat_badges.push('<span class="badge bg-danger me-1">XSS</span>');
if (entry.sql_alert) threat_badges.push('<span class="badge bg-danger me-1">SQL</span>');
if (entry.webshell_alert) threat_badges.push('<span class="badge bg-danger me-1">SHELL</span>');
if (entry.put_method) threat_badges.push('<span class="badge bg-danger me-1">PUT</span>');
if (entry.illegal_resource) threat_badges.push('<span class="badge bg-warning me-1">403</span>');
const threat_html = threat_badges.length > 0 ? `<div class="mb-2">${threat_badges.join('')}</div>` : '';
let row_class = '';
if (entry.has_threat) {
row_class = 'table-danger';
} else if (entry.status_code.startsWith('5')) {
row_class = 'table-danger';
} else if (entry.status_code.startsWith('4')) {
row_class = 'table-warning';
} else if (entry.status_code.startsWith('2')) {
row_class = 'table-light';
} else {
row_class = 'table-light';
}
return `
<tr class="${row_class}" style="font-family: monospace; font-size: 11px;">
<td>
${threat_html}
<small style="color: #0066cc;">${escapeHtml(entry.timestamp)}</small><br>
<small style="color: #666;">${escapeHtml(entry.ip_address)}</small>
<strong style="color: #333;">${escapeHtml(entry.http_method)}</strong>
<code style="color: #333;">${escapeHtml(entry.requested_url)}</code>
<span class="badge bg-dark" style="color: white; margin-left: 5px;">${escapeHtml(entry.status_code)}</span>
<br>
<small style="color: #666;">${escapeHtml(entry.frontend)}~ ${escapeHtml(entry.backend)}</small>
</td>
</tr>
`;
}).join('');
}
/**
* Update exclude UI
*/
function updateExcludeUI() {
if (excludePhrases.length > 0) {
const tags = excludePhrases.map((phrase, idx) => `
<span class="badge bg-warning text-dark me-2" style="cursor: pointer;" onclick="window.removeExcludePhrase(${idx})">
${escapeHtml(phrase)} <i class="bi bi-x"></i>
</span>
`).join('');
const container = document.createElement('div');
container.className = 'small mt-2';
container.innerHTML = `<strong>Hiding:</strong> ${tags}`;
const existing = document.getElementById('exclude_ui');
if (existing) existing.remove();
container.id = 'exclude_ui';
excludeFilter.parentElement.parentElement.after(container);
} else {
const existing = document.getElementById('exclude_ui');
if (existing) existing.remove();
}
}
/**
* Remove exclude phrase
*/
window.removeExcludePhrase = function(idx) {
console.log(`[Logs] Remove exclude phrase at index ${idx}`);
excludePhrases.splice(idx, 1);
updateExcludeUI();
currentPage = 1;
loadLogsWithPage();
};
/**
* Show error
*/
function showError(msg) {
logsContainer.innerHTML = `<tr><td class="alert alert-danger mb-0">${escapeHtml(msg)}</td></tr>`;
}
/**
* Escape HTML
*/
function escapeHtml(text) {
const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return (text || '').replace(/[&<>"']/g, m => map[m]);
}
// Initial load
console.log('[Logs] Initial load');
loadLogsWithPage();
});

156
static/js/user_manager.js Normal file
View File

@@ -0,0 +1,156 @@
/**
* User Management UI
*/
let currentEditUserId = null;
document.addEventListener('DOMContentLoaded', function() {
loadUsers();
document.getElementById('createUserBtn').addEventListener('click', createUser);
document.getElementById('updateUserBtn').addEventListener('click', updateUser);
});
function loadUsers() {
fetch('/api/users')
.then(r => r.json())
.then(data => {
if (data.success) {
renderUsers(data.users);
}
})
.catch(e => console.error('Error loading users:', e));
}
function renderUsers(users) {
const tbody = document.getElementById('usersList');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No users found</td></tr>';
return;
}
tbody.innerHTML = users.map(user => `
<tr>
<td><strong>${escapeHtml(user.username)}</strong></td>
<td>
${user.is_admin ? '<span class="badge bg-danger">Admin</span>' : '<span class="badge bg-secondary">User</span>'}
</td>
<td><small class="text-muted">${formatDate(user.created_at)}</small></td>
<td><small class="text-muted">${user.last_login ? formatDate(user.last_login) : 'Never'}</small></td>
<td>
<button class="btn btn-sm btn-primary" onclick="editUser(${user.id}, '${escapeHtml(user.username)}', ${user.is_admin})">
<i class="bi bi-pencil"></i>
</button>
${user.id !== getUserId() ? `
<button class="btn btn-sm btn-danger" onclick="deleteUser(${user.id}, '${escapeHtml(user.username)}')">
<i class="bi bi-trash"></i>
</button>
` : ''}
</td>
</tr>
`).join('');
}
function createUser() {
const username = document.getElementById('newUsername').value;
const password = document.getElementById('newPassword').value;
const isAdmin = document.getElementById('newIsAdmin').checked;
fetch('/api/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, password, is_admin: isAdmin })
})
.then(r => r.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('newUserModal')).hide();
document.getElementById('newUserForm').reset();
loadUsers();
showAlert('User created successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function editUser(userId, username, isAdmin) {
currentEditUserId = userId;
document.getElementById('editUsername').textContent = username;
document.getElementById('editIsAdmin').checked = isAdmin;
new bootstrap.Modal(document.getElementById('editUserModal')).show();
}
function updateUser() {
const password = document.getElementById('editPassword').value;
const isAdmin = document.getElementById('editIsAdmin').checked;
const body = { is_admin: isAdmin };
if (password) body.password = password;
fetch(`/api/users/${currentEditUserId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
})
.then(r => r.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('editUserModal')).hide();
loadUsers();
showAlert('User updated successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function deleteUser(userId, username) {
if (!confirm(`Delete user '${username}'?`)) return;
fetch(`/api/users/${userId}`, {method: 'DELETE'})
.then(r => r.json())
.then(data => {
if (data.success) {
loadUsers();
showAlert('User deleted successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.card-body').prepend(alert);
setTimeout(() => alert.remove(), 5000);
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
}
function escapeHtml(text) {
const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return text.replace(/[&<>"']/g, m => map[m]);
}
function getUserId() {
// Extract from current user endpoint
let userId = null;
fetch('/api/current-user')
.then(r => r.json())
.then(data => { userId = data.id; });
return userId;
}

285
static/js/vhost_manager.js Normal file
View File

@@ -0,0 +1,285 @@
/**
* VHost Manager - Frontend CRUD Logic
*/
let currentEditVHostId = null;
document.addEventListener('DOMContentLoaded', function() {
loadVHosts();
// Event listeners
document.getElementById('createVHostBtn').addEventListener('click', createVHost);
document.getElementById('updateVHostBtn').addEventListener('click', updateVHost);
document.getElementById('addServerBtn').addEventListener('click', addServerInput);
// Event delegation for remove buttons
document.getElementById('backendServersContainer').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-server')) {
e.target.closest('.backend-server').remove();
}
});
});
function loadVHosts() {
fetch('/api/vhosts')
.then(r => r.json())
.then(data => {
if (data.success) {
renderVHosts(data.vhosts);
updateStats(data.vhosts);
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => console.error('Error loading vhosts:', e));
}
function updateStats(vhosts) {
document.getElementById('total_vhosts').textContent = vhosts.length;
document.getElementById('enabled_vhosts').textContent = vhosts.filter(v => v.enabled).length;
document.getElementById('disabled_vhosts').textContent = vhosts.filter(v => !v.enabled).length;
document.getElementById('ssl_vhosts').textContent = vhosts.filter(v => v.use_ssl).length;
}
function renderVHosts(vhosts) {
const tbody = document.getElementById('vhostsList');
if (vhosts.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No VHosts created yet</td></tr>';
return;
}
tbody.innerHTML = vhosts.map(v => `
<tr>
<td>
<strong>${escapeHtml(v.name)}</strong>
${v.use_ssl ? '<span class="badge bg-success ms-2">SSL</span>' : ''}
</td>
<td>${escapeHtml(v.hostname)}</td>
<td>
<code>${v.frontend_ip}:${v.frontend_port}</code>
</td>
<td><span class="badge bg-info">${v.protocol.toUpperCase()}</span></td>
<td>
<span class="badge bg-secondary">${v.backend_count} servers</span>
</td>
<td>
<button class="btn btn-sm ${v.enabled ? 'btn-success' : 'btn-warning'}"
onclick="toggleVHost(${v.id}, ${v.enabled})">
<i class="bi bi-${v.enabled ? 'power' : 'hourglass'}"></i>
${v.enabled ? 'Enabled' : 'Disabled'}
</button>
</td>
<td>
<button class="btn btn-sm btn-primary" onclick="editVHost(${v.id})">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-danger" onclick="deleteVHost(${v.id}, '${escapeHtml(v.name)}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`).join('');
}
function createVHost() {
const name = document.getElementById('vhost_name').value;
const hostname = document.getElementById('vhost_hostname').value;
const port = document.getElementById('vhost_port').value;
const protocol = document.getElementById('vhost_protocol').value;
const lb_method = document.getElementById('vhost_lb_method').value;
const use_ssl = document.getElementById('vhost_ssl').checked;
// Get backend servers
const servers = [];
document.querySelectorAll('.backend-server').forEach(el => {
const ip = el.querySelector('.backend-ip').value;
const port = el.querySelector('.backend-port').value;
if (ip && port) {
servers.push({ ip_address: ip, port: parseInt(port) });
}
});
if (servers.length === 0) {
showAlert('At least one backend server is required', 'warning');
return;
}
const payload = {
name, hostname,
frontend_port: parseInt(port),
protocol, lb_method, use_ssl,
backend_servers: servers
};
fetch('/api/vhosts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('newVHostModal')).hide();
document.getElementById('newVHostForm').reset();
document.getElementById('backendServersContainer').innerHTML = `
<div class="backend-server mb-3 p-3 border rounded">
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" class="form-control backend-ip" placeholder="IP Address" required>
</div>
<div class="col-md-6 mb-2">
<input type="number" class="form-control backend-port" placeholder="Port" value="80" required>
</div>
</div>
<button type="button" class="btn btn-sm btn-danger remove-server">Remove</button>
</div>
`;
loadVHosts();
showAlert('VHost created successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function editVHost(vhostId) {
currentEditVHostId = vhostId;
fetch(`/api/vhosts/${vhostId}`)
.then(r => r.json())
.then(data => {
if (data.success) {
const v = data.vhost;
document.getElementById('editVHostName').textContent = v.name;
const formContent = `
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">VHost Name</label>
<input type="text" class="form-control" id="edit_name" value="${escapeHtml(v.name)}">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Hostname</label>
<input type="text" class="form-control" id="edit_hostname" value="${escapeHtml(v.hostname)}">
</div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="edit_enabled" ${v.enabled ? 'checked' : ''}>
<label class="form-check-label" for="edit_enabled">Enabled</label>
</div>
<hr>
<h6>Backend Servers</h6>
<div id="editServersContainer">
${v.backend_servers.map(bs => `
<div class="backend-server mb-3 p-3 border rounded">
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" class="form-control backend-ip" value="${bs.ip_address}" required>
</div>
<div class="col-md-6 mb-2">
<input type="number" class="form-control backend-port" value="${bs.port}" required>
</div>
</div>
<button type="button" class="btn btn-sm btn-danger remove-server">Remove</button>
</div>
`).join('')}
</div>
`;
document.getElementById('editFormContent').innerHTML = formContent;
new bootstrap.Modal(document.getElementById('editVHostModal')).show();
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function updateVHost() {
const name = document.getElementById('edit_name').value;
const hostname = document.getElementById('edit_hostname').value;
const enabled = document.getElementById('edit_enabled').checked;
const payload = { name, hostname, enabled };
fetch(`/api/vhosts/${currentEditVHostId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('editVHostModal')).hide();
loadVHosts();
showAlert('VHost updated successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function toggleVHost(vhostId, isEnabled) {
fetch(`/api/vhosts/${vhostId}/toggle`, { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.success) {
loadVHosts();
showAlert(data.message, 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function deleteVHost(vhostId, name) {
if (!confirm(`Delete VHost '${name}'? This cannot be undone.`)) return;
fetch(`/api/vhosts/${vhostId}`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
if (data.success) {
loadVHosts();
showAlert('VHost deleted successfully', 'success');
} else {
showAlert(data.error, 'danger');
}
})
.catch(e => showAlert(e.message, 'danger'));
}
function addServerInput() {
const container = document.getElementById('backendServersContainer');
const newServer = document.createElement('div');
newServer.className = 'backend-server mb-3 p-3 border rounded';
newServer.innerHTML = `
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" class="form-control backend-ip" placeholder="IP Address" required>
</div>
<div class="col-md-6 mb-2">
<input type="number" class="form-control backend-port" placeholder="Port" value="80" required>
</div>
</div>
<button type="button" class="btn btn-sm btn-danger remove-server">Remove</button>
`;
container.appendChild(newServer);
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.card:first-of-type').parentElement.insertBefore(alertDiv, document.querySelector('.card:first-of-type'));
setTimeout(() => alertDiv.remove(), 5000);
}
function escapeHtml(text) {
const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return text.replace(/[&<>"']/g, m => map[m]);
}

View File

@@ -1,35 +1,47 @@
[supervisord]
nodaemon=true
user=root
loglevel=info
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/supervisor
[program:haproxy]
command=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/haproxy.err.log
stdout_logfile=/var/log/haproxy.log
priority=100
stopasgroup=true
killasgroup=true
[unix_http_server]
file=/var/run/supervisor.sock
[program:flask_app]
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
# ===== FLASK APPLICATION =====
[program:flask]
command=python /app/app.py
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/flask_app.err.log
stdout_logfile=/var/log/supervisor/flask_app.out.log
priority=999
environment=FLASK_APP=/app/app.py,FLASK_ENV=production,PYTHONUNBUFFERED=1
startsecs=10
stdout_logfile=/var/log/supervisor/flask.log
stderr_logfile=/var/log/supervisor/flask_error.log
stopasgroup=true
stopsignal=TERM
priority=999
environment=PYTHONUNBUFFERED=1,FLASK_APP=app.py,FLASK_ENV=production
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
# ===== HAPROXY =====
[program:haproxy]
command=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/haproxy.log
stderr_logfile=/var/log/supervisor/haproxy_error.log
stopasgroup=true
priority=998
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
# ===== LOG ROTATION =====
[program:logrotate]
command=/bin/bash -c "while true; do sleep 86400; logrotate /etc/logrotate.d/haproxy 2>/dev/null || true; done"
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/logrotate.log
stderr_logfile=/var/log/supervisor/logrotate_error.log
stopasgroup=true
priority=997

16
templates/403.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Access Denied{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 style="font-size: 72px; color: #dc3545;">403</h1>
<h2>Access Denied</h2>
<p class="text-muted">You don't have permission to access this resource.</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Go Home
</a>
</div>
</div>
{% endblock %}

16
templates/404.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 style="font-size: 72px; color: #dc3545;">404</h1>
<h2>Page Not Found</h2>
<p class="text-muted">The page you're looking for doesn't exist.</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Go Home
</a>
</div>
</div>
{% endblock %}

16
templates/500.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Server Error{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 style="font-size: 72px; color: #dc3545;">500</h1>
<h2>Internal Server Error</h2>
<p class="text-muted">Something went wrong on our end.</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Go Home
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,12 +1,13 @@
{% set active_page = active_page|default('') %}
<!doctype html>
<html lang="pl" data-bs-theme="dark">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}HAProxy Configurator{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/edit.css') }}">
{% block head %}{% endblock %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@@ -14,46 +15,190 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
<style>
.navbar-brand {
font-weight: 700;
font-size: 1.25rem;
}
.user-menu {
min-width: 200px;
}
.menu-divider {
margin: 0.5rem 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
</head>
<body class="bg-body">
<header class="header1" id="header1">
<div class="container d-flex align-items-center justify-content-between flex-wrap gap-2 py-2">
<a href="{{ url_for('home') }}" class="d-flex align-items-center text-decoration-none logo text-reset">
<h3 class="m-0 d-flex align-items-center gap-2">
<i class="fas fa-globe"></i><span>HAProxy Configurator</span>
</h3>
</a>
<nav class="menu d-flex align-items-center gap-1 flex-wrap">
<a href="{{ url_for('home') }}" class="menu-link {{ 'active' if request.path.startswith('/home') else '' }}"><i class="bi bi-speedometer"></i> Dashboard</a>
<a href="{{ url_for('main.index') }}" class="menu-link {{ 'active' if request.path == '/' else '' }}"><i class="bi bi-plus-circle"></i> Add FE/BE</a>
<a href="{{ url_for('edit.edit_haproxy_config') }}" class="menu-link {{ 'active' if request.path.startswith('/edit') else '' }}"><i class="bi bi-pencil-square"></i> Edit Configuration</a>
<a href="{{ url_for('display_logs') }}" class="menu-link {{ 'active' if request.path.startswith('/logs') else '' }}"><i class="bi bi-shield-lock"></i> Logs</a>
<a href="{{ url_for('display_haproxy_stats') }}" class="menu-link {{ 'active' if request.path.startswith('/statistics') else '' }}"><i class="bi bi-graph-up-arrow"></i> Stats</a>
<a href="http://{{ request.host.split(':')[0] }}:8404/stats" class="menu-link" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right"></i> HAProxy Stats</a>
</nav>
</div>
</header>
<!-- NAVBAR -->
<header class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom border-secondary">
<div class="container-fluid px-4">
<!-- Logo -->
<a href="{% if session.get('user_id') %}{{ url_for('main.index') }}{% else %}{{ url_for('auth.login') }}{% endif %}" class="navbar-brand d-flex align-items-center gap-2">
<i class="fas fa-globe"></i>
<span>HAProxy Manager</span>
</a>
<main class="container py-4">
{% with messages = get_flashed_messages() %}{% if messages %}<div id="_flash_msgs" data-msgs="{{ messages|tojson }}"></div>{% endif %}{% endwith %}
{% block breadcrumb %}{% endblock %}
<div id="toast-stack" class="toast-container position-fixed top-0 end-0 p-3"></div>
{% block content %}{% endblock %}
</main>
<footer class="app-footer border-top">
<div class="container d-flex flex-wrap justify-content-between align-items-center py-3 small text-muted">
<span>© 2025 HAProxy Configurator</span>
<span class="d-flex align-items-center gap-2">
<i class="bi bi-code-slash"></i>
<span>Based on: <a href="https://github.com/alonz22/haproxy-dashboard">This project</a> | by @linuxiarz.pl </span>
</span>
</div>
</footer>
<!-- Toggle Button (Mobile) -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
{% block page_js %}{% endblock %}
</body>
<!-- Navigation Menu -->
<div class="collapse navbar-collapse" id="navbarNav">
{% if session.get('user_id') %}
<nav class="navbar-nav ms-auto d-flex align-items-center gap-3">
<!-- Menu Links -->
<a href="{{ url_for('main.index') }}" class="nav-link {{ 'active' if request.path == '/' or request.path.startswith('/home') else '' }}">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a href="{{ url_for('main.index') }}" class="nav-link {{ 'active' if request.path.startswith('/add') else '' }}">
<i class="bi bi-plus-circle"></i> Add VHost
</a>
<a href="{{ url_for('edit.edit_haproxy_config') }}" class="nav-link {{ 'active' if request.path.startswith('/edit') else '' }}">
<i class="bi bi-pencil-square"></i> Edit Config
</a>
<a href="{{ url_for('main.display_logs') }}" class="nav-link {{ 'active' if request.path.startswith('/logs') else '' }}">
<i class="bi bi-shield-lock"></i> Logs
</a>
<a href="{{ url_for('main.display_haproxy_stats') }}" class="nav-link {{ 'active' if request.path.startswith('/statistics') else '' }}">
<i class="bi bi-graph-up-arrow"></i> Stats
</a>
<a href="http://{{ request.host.split(':')[0] }}:8404/stats" class="nav-link" target="_blank" rel="noopener">
<i class="bi bi-box-arrow-up-right"></i> HAProxy Stats
</a>
<!-- User Dropdown Menu -->
<div class="nav-item dropdown">
<button class="nav-link dropdown-toggle btn btn-link text-decoration-none d-flex align-items-center gap-2" id="userMenu" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i>
<span class="d-none d-lg-inline">{{ session.get('username', 'User') }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end user-menu" aria-labelledby="userMenu">
<li>
<a class="dropdown-item" href="#" disabled>
<i class="bi bi-person"></i> {{ session.get('username', 'User') }}
</a>
</li>
<li><hr class="dropdown-divider"></li>
<!-- Admin Only Menu Items -->
{% if session.get('is_admin') %}
<li>
<a class="dropdown-item" href="{{ url_for('main.index') }}">
<i class="bi bi-people"></i> User Management
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('main.index') }}">
<i class="bi bi-shield-check"></i> Certificates
</a>
</li>
<li><hr class="dropdown-divider"></li>
{% endif %}
<li>
<a class="dropdown-item text-warning" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</li>
</ul>
</div>
</nav>
{% else %}
<!-- Login Link (when not authenticated) -->
<nav class="navbar-nav ms-auto">
<a href="{{ url_for('auth.login') }}" class="nav-link">
<i class="bi bi-box-arrow-in-right"></i> Login
</a>
</nav>
{% endif %}
</div>
</div>
</header>
<!-- MAIN CONTENT -->
<main class="container-fluid py-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ 'check-circle' if category == 'success' else 'exclamation-circle' if category == 'danger' else 'info-circle' }}"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Breadcrumb -->
{% block breadcrumb %}{% endblock %}
<!-- Toast Container -->
<div id="toast-stack" class="toast-container position-fixed top-0 end-0 p-3"></div>
<!-- Page Content -->
{% block content %}{% endblock %}
</main>
<!-- FOOTER -->
<footer class="app-footer border-top border-secondary bg-dark mt-5">
<div class="container-fluid px-4 py-3">
<div class="row align-items-center">
<div class="col-md-6 small text-muted">
<p class="mb-2">
© 2025 <strong>HAProxy Configurator & Manager</strong>
</p>
<p class="mb-0">
<i class="bi bi-info-circle"></i>
Powerful web-based HAProxy configuration management system
</p>
</div>
<div class="col-md-6 text-md-end small text-muted text-start">
<p class="mb-2">
<i class="bi bi-code-slash"></i>
Built with <strong>Flask</strong> + <strong>SQLAlchemy</strong> + <strong>Bootstrap 5</strong>
</p>
<p class="mb-0">
Based on: <a href="https://github.com/alonz22/haproxy-dashboard" target="_blank" class="text-decoration-none" rel="noopener">Original Project</a>
| Maintained by <strong>@linuxiarz.pl</strong>
</p>
</div>
</div>
</div>
</footer>
<!-- SCRIPTS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
{% block page_js %}{% endblock %}
<script>
/**
* Auto-dismiss alerts after 5 seconds
*/
document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert:not(.alert-permanent)');
alerts.forEach(alert => {
setTimeout(() => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}, 5000);
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,115 @@
{% extends "base.html" %}
{% block title %}HAProxy • Certificate Manager{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item active">SSL Certificates</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-shield-lock"></i> SSL Certificates</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
<i class="bi bi-cloud-upload"></i> Upload Certificate
</button>
</div>
<!-- Certificates Table -->
<div class="card">
<div class="card-header bg-dark">
<h5 class="mb-0"><i class="bi bi-list-check"></i> Certificates List</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="certsTable">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Common Name (CN)</th>
<th>Expires</th>
<th>Used By</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="certsList">
<tr><td colspan="6" class="text-center text-muted py-4">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Upload Modal -->
<div class="modal fade" id="uploadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Upload SSL Certificate</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="uploadForm" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Certificate Name *</label>
<input type="text" class="form-control" id="cert_name" required>
<small class="text-muted">e.g., example-com-2025</small>
</div>
<div class="mb-3">
<label class="form-label">PEM Certificate File * <i class="bi bi-info-circle" title="Combined certificate + key in PEM format"></i></label>
<input type="file" class="form-control" id="cert_file" accept=".pem,.crt,.cert,.key" required>
<small class="text-muted">
Supported formats: PEM (combined cert+key), .crt, .cert, .key
</small>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Supported Formats:</strong>
<ul class="mb-0 mt-2">
<li>Single file with both certificate and private key (PEM)</li>
<li>PKCS#12 (.p12) - will be converted</li>
<li>Separate .crt and .key files - upload combined</li>
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="uploadBtn">Upload</button>
</div>
</div>
</div>
</div>
<!-- Certificate Details Modal -->
<div class="modal fade" id="detailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title">Certificate Details: <span id="detailsName"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="detailsContent"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="exportBtn">
<i class="bi bi-download"></i> Export PEM
</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/cert_manager.js') }}"></script>
{% endblock %}

191
templates/dashboard.html Normal file
View File

@@ -0,0 +1,191 @@
{% extends "base.html" %}
{% block title %}HAProxy • Dashboard{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item active">VHosts</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-server"></i> Virtual Hosts</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newVHostModal">
<i class="bi bi-plus-lg"></i> Create VHost
</button>
</div>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title">Total VHosts</h5>
<h3 id="total_vhosts">0</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">Enabled</h5>
<h3 id="enabled_vhosts">0</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<h5 class="card-title">Disabled</h5>
<h3 id="disabled_vhosts">0</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title">SSL Enabled</h5>
<h3 id="ssl_vhosts">0</h3>
</div>
</div>
</div>
</div>
<!-- VHosts Table -->
<div class="card">
<div class="card-header bg-dark">
<h5 class="mb-0"><i class="bi bi-list-check"></i> VHosts List</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="vhostsTable">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Hostname</th>
<th>Bind</th>
<th>Protocol</th>
<th>Servers</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="vhostsList">
<tr><td colspan="7" class="text-center text-muted py-4">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- New VHost Modal -->
<div class="modal fade" id="newVHostModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Create New VHost</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="newVHostForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">VHost Name *</label>
<input type="text" class="form-control" id="vhost_name" required>
<small class="text-muted">e.g., web-app-01</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Hostname *</label>
<input type="text" class="form-control" id="vhost_hostname" required>
<small class="text-muted">e.g., example.com</small>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Frontend Port *</label>
<input type="number" class="form-control" id="vhost_port" value="443" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Protocol</label>
<select class="form-select" id="vhost_protocol">
<option value="http">HTTP</option>
<option value="tcp">TCP</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Load Balancing Method</label>
<select class="form-select" id="vhost_lb_method">
<option value="roundrobin">Round Robin</option>
<option value="leastconn">Least Connections</option>
<option value="source">Source IP Hash</option>
<option value="uri">URI Hash</option>
</select>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="vhost_ssl">
<label class="form-check-label" for="vhost_ssl">
Enable SSL
</label>
</div>
<hr>
<h6>Backend Servers</h6>
<div id="backendServersContainer">
<div class="backend-server mb-3 p-3 border rounded">
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" class="form-control backend-ip" placeholder="IP Address" required>
</div>
<div class="col-md-6 mb-2">
<input type="number" class="form-control backend-port" placeholder="Port" value="80" required>
</div>
</div>
<button type="button" class="btn btn-sm btn-danger remove-server">Remove</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="addServerBtn">
<i class="bi bi-plus"></i> Add Server
</button>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="createVHostBtn">Create VHost</button>
</div>
</div>
</div>
</div>
<!-- Edit VHost Modal -->
<div class="modal fade" id="editVHostModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Edit VHost: <span id="editVHostName"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editVHostForm">
<div id="editFormContent"></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="updateVHostBtn">Update VHost</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/vhost_manager.js') }}"></script>
{% endblock %}

View File

@@ -1,34 +1,83 @@
{% extends "base.html" %}
{% set active_page = "" %}
{% block title %}HAProxy • Edit{% endblock %}
{% block breadcrumb %}<nav aria-label="breadcrumb" class="mb-3"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item active" aria-current="page">Edytor</li></ol></nav>{% endblock %}
{% block content %}
<div class="card shadow-sm">
<div class="card-body">
<h4 class="mb-3 text-muted">Edit HAProxy configuration</h4>
<form method="POST" novalidate>
<div class="mb-3">
<label for="haproxy_config" class="form-label">Config</label>
<textarea class="form-control" name="haproxy_config" id="haproxy_config" rows="20">{{ config_content }}</textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-warning" id="save_check" name="save_check">
<i class="bi bi-search me-1"></i> Check & Save
</button>
<button type="submit" class="btn btn-primary" name="save_reload">
<i class="bi bi-arrow-repeat me-1"></i> Check & Restart
</button>
</div>
</form>
{% if check_output %}
<div class="alert alert-{{ check_level|default('success') }}" role="alert">
<pre class="mb-0">{{ check_output }}</pre>
</div>
{% endif %}
</div>
{% set active_page = "edit" %}
{% block title %}HAProxy • Configuration Editor{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item active" aria-current="page">Edit Configuration</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<!-- CodeMirror CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/material-darker.min.css">
{% if check_output %}
<div class="alert alert-{{ check_level|default('info') }} alert-dismissible fade show" role="alert">
<i class="bi bi-{% if check_level == 'success' %}check-circle{% elif check_level == 'danger' %}exclamation-circle{% elif check_level == 'warning' %}exclamation-triangle{% else %}info-circle{% endif %} me-2"></i>
<strong>
{% if check_level == 'success' %}Configuration Saved
{% elif check_level == 'danger' %}Configuration Error
{% elif check_level == 'warning' %}Warning
{% else %}Information{% endif %}
</strong>
<div class="mt-2 small" style="font-family: monospace; background-color: rgba(0,0,0,0.1); padding: 10px; border-radius: 4px; max-height: 250px; overflow-y: auto; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word;">{{ check_output }}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endblock %}
{% block page_js %}
<script src="{{ url_for('static', filename='js/edit.js') }}"></script>
{% endif %}
<!-- Editor Section -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0"><i class="bi bi-pencil-square me-2"></i>HAProxy Configuration Editor</h5>
</div>
<small class="text-white-50">Real-time editor with syntax highlighting</small>
</div>
<div class="card-body" style="padding: 0;">
<form method="post" id="edit_form">
<!-- Editor Container -->
<div style="border-bottom: 1px solid #dee2e6;">
<textarea id="haproxy_editor" name="haproxy_config" style="display: none;">{{ config_content }}</textarea>
<!-- Fallback textarea (hidden by default) -->
<textarea id="haproxy_config" name="haproxy_config" style="display: none; width: 100%; border: none; font-family: monospace; font-size: 13px; resize: none; padding: 12px; min-height: 500px; background: #1e1e1e; color: #e8e8e8;"></textarea>
</div>
<!-- Toolbar -->
<div class="p-3 bg-dark d-flex justify-content-between align-items-center flex-wrap gap-2" style="border-top: 1px solid #444;">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success btn-sm" name="action" value="check">
<i class="bi bi-check-circle me-1"></i>Validate Configuration
</button>
<button type="submit" class="btn btn-primary btn-sm" name="action" value="save">
<i class="bi bi-save me-1"></i>Save & Restart HAProxy
</button>
<a href="{{ url_for('main.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Cancel
</a>
</div>
<small class="text-muted" style="color: #aaa !important;">
<i class="bi bi-info-circle me-1"></i>
<span id="line_col">Line 1, Col 1</span> |
<span id="char_count">0</span> characters
</small>
</div>
</form>
</div>
</div>
<!-- CodeMirror JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/nginx/nginx.min.js"></script>
<!-- Editor JS -->
<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
{% endblock %}

View File

@@ -1,220 +1,475 @@
{% extends "base.html" %}
{% set active_page = "index" %}
{% block title %}HAProxy • Index{% endblock %}
{% block breadcrumb %}<nav aria-label="breadcrumb" class="mb-3"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item active" aria-current="page">Konfiguracja</li></ol></nav>{% endblock %}
{% block title %}HAProxy • Configuration{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item active" aria-current="page">Add Configuration</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="card shadow-sm">
<div class="card-body">
<form method="post" action="/" id="fe-be-form" novalidate>
<h5 class="mb-3"><i class="fas fa-globe me-2"></i>New frontend</h5>
{% if message %}
<div class="alert {% if 'already exists' in message %}alert-danger{% else %}alert-success{% endif %} alert-dismissible" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row g-3">
<div class="col-md-4">
<label class="form-label" for="frontend_name">Name</label>
<input type="text" class="form-control" name="frontend_name" id="frontend_name" required>
</div>
<div class="col-md-4">
<label class="form-label" for="frontend_ip">IP</label>
<input type="text" class="form-control" name="frontend_ip" id="frontend_ip" required>
</div>
<div class="col-md-3">
<label class="form-label" for="frontend_port">Port</label>
<input type="number" class="form-control" name="frontend_port" id="frontend_port" required>
</div>
</div>
<div class="form-check mt-3">
<input type="checkbox" class="form-check-input" id="ssl_checkbox" name="ssl_checkbox">
<label class="form-check-label" for="ssl_checkbox"><i class="fas fa-lock me-2"></i>SSL cert</label>
</div>
<div class="row g-3 mt-1 d-none" id="ssl_fields">
<div class="col-md-6">
<label class="form-label" for="ssl_cert_path">Certificate path (put in /ssl/)</label>
<input type="text" id="ssl_cert_path" class="form-control" name="ssl_cert_path">
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="ssl_redirect_checkbox" name="ssl_redirect_checkbox">
<label class="form-check-label" for="ssl_redirect_checkbox"><i class="fas fa-arrow-circle-right me-2"></i>Redirect do HTTPS</label>
</div>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-4">
<label class="form-label" for="lb_method">Metoda LB</label>
<select class="form-select" name="lb_method" id="lb_method">
<option value="roundrobin">Round Robin</option>
<option value="leastconn">Least Connections</option>
<option value="source">Source</option>
<option value="wrr">WRR</option>
<option value="wlc">WLC</option>
<option value="random">Random</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="protocol">Tryb</label>
<select class="form-select" name="protocol" id="protocol" required>
<option value="" disabled selected>--Select--</option>
<option value="tcp">TCP</option>
<option value="http">HTTP</option>
</select>
</div>
</div>
<div class="form-check mt-3">
<input type="checkbox" class="form-check-input" name="add_dos" id="add_dos">
<label class="form-check-label" for="add_dos"><i class="fas fa-shield-alt me-2"></i>DOS protection</label>
</div>
<div class="row g-3 mt-1 d-none" id="dos_fields">
<div class="col-md-6">
<label class="form-label" for="limit_requests">Limit (np. 20)</label>
<input type="text" class="form-control" name="limit_requests" id="limit_requests">
</div>
<div class="col-md-6">
<label class="form-label" for="ban_duration">Ban (np. 15s)</label>
<input type="text" class="form-control" name="ban_duration" id="ban_duration">
</div>
</div>
<div class="row g-3 mt-2 d-none http-only" id="http_extras">
<div class="col-12">
<div class="row g-3">
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="sql_injection_check" name="sql_injection_check">
<label class="form-check-label" for="sql_injection_check"><i class="fas fa-shield-alt me-2"></i>SQLi</label>
</div>
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-primary">{{ frontend_count|default(0) }}</h5>
<p class="card-text"><i class="bi bi-diagram-2"></i> Frontends</p>
</div>
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="xss_check" name="xss_check">
<label class="form-check-label" for="xss_check"><i class="fas fa-shield-alt me-2"></i>XSS</label>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-success">{{ backend_count|default(0) }}</h5>
<p class="card-text"><i class="bi bi-hdd-rack"></i> Backends</p>
</div>
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="remote_uploads_check" name="remote_uploads_check">
<label class="form-check-label" for="remote_uploads_check"><i class="fas fa-shield-alt me-2"></i>Remote uploads</label>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-warning">{{ acl_count|default(0) }}</h5>
<p class="card-text"><i class="bi bi-shield"></i> ACLs</p>
</div>
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="webshells_check" name="webshells_check">
<label class="form-check-label" for="webshells_check"><i class="fas fa-shield-alt me-2"></i>Webshells</label>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-info">L7: {{ layer7_count|default(0) }} / L4: {{ layer4_count|default(0) }}</h5>
<p class="card-text"><i class="bi bi-layers"></i> Layers</p>
</div>
<div class="col-md-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="forward_for_check" name="forward_for_check">
<label class="form-check-label" for="forward_for_check"><i class="fas fa-network-wired me-2"></i>forwardfor</label>
</div>
</div>
</div>
</div>
</div>
<div class="form-check mt-3">
<input type="checkbox" class="form-check-input" name="add_acl" id="add_acl">
<label class="form-check-label" for="add_acl"><i class="fas fa-user-lock me-2"></i>ACL for frontend</label>
</div>
<div class="row g-3 mt-1 d-none" id="acl_fields">
<div class="col-md-4">
<label class="form-label" for="acl">ACL</label>
<input type="text" class="form-control" name="acl" id="acl" placeholder="acl_name">
</div>
<div class="col-md-4">
<label class="form-label" for="acl_action">Action</label>
<input type="text" class="form-control" name="acl_action" id="acl_action" placeholder="hdr(host) -i test.com">
</div>
<div class="col-md-4">
<label class="form-label" for="backend_name_acl">Backend</label>
<input type="text" class="form-control" name="backend_name_acl" id="backend_name_acl" placeholder="somebackend">
</div>
</div>
<div class="form-check mt-3 http-only d-none" id="forbidden_acl_container">
<input type="checkbox" class="form-check-input" name="add_acl_path" id="add_acl_path">
<label class="form-check-label" for="add_acl_path"><i class="fas fa-ban me-2"></i>Block path</label>
</div>
<div class="row g-3 mt-1 d-none" id="forbidden_fields">
<div class="col-md-4">
<label class="form-label" for="forbidden_name">ACL name</label>
<input type="text" class="form-control" name="forbidden_name" id="forbidden_name">
</div>
<div class="col-md-4">
<label class="form-label" for="allowed_ip">Allowed IP</label>
<input type="text" class="form-control" name="allowed_ip" id="allowed_ip">
</div>
<div class="col-md-4">
<label class="form-label" for="forbidden_path">Path (ex. /admin)</label>
<input type="text" class="form-control" name="forbidden_path" id="forbidden_path">
</div>
</div>
<div class="form-check mt-3 http-only d-none" id="path_based_container">
<input type="checkbox" class="form-check-input" name="add_path_based" id="add_path_based">
<label class="form-check-label" for="add_path_based"><i class="fas fa-arrow-circle-right me-2"></i>Path-based redirect</label>
</div>
<div class="row g-3 mt-1 d-none" id="base_redirect_fields">
<div class="col-md-4">
<label class="form-label" for="redirect_domain_name">Domena docelowa</label>
<input type="text" class="form-control" name="redirect_domain_name" id="redirect_domain_name" placeholder="test2.com:8888">
</div>
<div class="col-md-4">
<label class="form-label" for="root_redirect">Root path</label>
<input type="text" class="form-control" name="root_redirect" id="root_redirect" placeholder="/">
</div>
<div class="col-md-4">
<label class="form-label" for="redirect_to">Redirect to</label>
<input type="text" class="form-control" name="redirect_to" id="redirect_to" placeholder="/test">
</div>
</div>
<hr class="my-4">
<h5 class="mb-3"><i class="fas fa-sitemap me-2"></i>Backend pool</h5>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="backend_name">Backend name</label>
<input type="text" class="form-control" name="backend_name" id="backend_name" required>
</div>
</div>
<div id="backend_servers_container" class="mt-3">
<div class="row g-3 backend-server-row">
<div class="col-md-3">
<label class="form-label" for="name1">Server name</label>
<input type="text" id="name1" class="form-control" name="backend_server_names[]" placeholder="server1" required>
</div>
<div class="col-md-3">
<label class="form-label" for="ip1">IP</label>
<input type="text" id="ip1" class="form-control" name="backend_server_ips[]" required>
</div>
<div class="col-md-3">
<label class="form-label" for="port1">Port</label>
<input type="number" id="port1" class="form-control" name="backend_server_ports[]" required>
</div>
<div class="col-md-3">
<label class="form-label" for="maxconn1">MaxConn</label>
<input type="number" id="maxconn1" class="form-control" name="backend_server_maxconns[]">
</div>
</div>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-secondary" id="add_backend_btn"><i class="bi bi-plus-lg me-1"></i>Add backend</button>
<button type="submit" class="btn btn-success" id="success_btn"><i class="bi bi-check2-circle me-1"></i>Save</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block page_js %}
{% if message %}
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
<i class="bi bi-{% if message_type == 'success' %}check-circle{% elif message_type == 'danger' %}exclamation-circle{% elif message_type == 'warning' %}exclamation-triangle{% else %}info-circle{% endif %} me-2"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<form method="post" class="needs-validation">
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add New Configuration</h5>
</div>
<div class="card-body">
<!-- FRONTEND SECTION -->
<h6 class="text-primary mb-3"><i class="bi bi-hdd-network me-2"></i>Frontend Configuration</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label for="frontend_ip" class="form-label">Listener IP</label>
<input type="text" class="form-control" id="frontend_ip" name="frontend_ip"
placeholder="0.0.0.0" value="0.0.0.0" required>
</div>
<div class="col-md-4">
<label for="frontend_port" class="form-label">Listener Port</label>
<input type="number" class="form-control" id="frontend_port" name="frontend_port"
placeholder="443" value="443" min="1" max="65535" required>
</div>
<div class="col-md-4">
<label for="frontend_hostname" class="form-label">Frontend Hostname</label>
<input type="text" class="form-control" id="frontend_hostname" name="frontend_hostname"
placeholder="e.g. host.domain.com" required>
<small class="text-muted d-block mt-1">Frontend name will be generated automatically</small>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label for="protocol" class="form-label">Protocol</label>
<select class="form-select" id="protocol" name="protocol" required>
<option value="http">HTTP</option>
<option value="tcp">TCP</option>
</select>
</div>
<div class="col-md-6">
<label for="lb_method" class="form-label">Load Balancing Method</label>
<select class="form-select" id="lb_method" name="lb_method" required>
<option value="no-lb">No Load Balancing (single host)</option>
<option value="roundrobin">Round Robin</option>
<option value="leastconn">Least Connections</option>
<option value="source">Source IP Hash</option>
<option value="uri">URI Hash</option>
<option value="static-rr">Static Round Robin (WRR)</option>
</select>
</div>
</div>
<!-- SSL Section -->
<div class="row g-3 mb-3">
<div class="col-md-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="ssl_checkbox" name="ssl_checkbox">
<label class="form-check-label" for="ssl_checkbox">
<i class="bi bi-lock me-1"></i>Use SSL (HTTPS)
</label>
</div>
</div>
</div>
<div class="row g-3 mb-3 d-none" id="ssl_fields">
<div class="col-md-12">
<label for="ssl_cert_path" class="form-label">SSL Certificate Path</label>
<input type="text" class="form-control" id="ssl_cert_path" name="ssl_cert_path"
value="/app/ssl/haproxy-configurator.pem">
<small class="text-muted">Full path to .pem file</small>
</div>
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ssl_redirect_checkbox"
name="ssl_redirect_checkbox">
<label class="form-check-label" for="ssl_redirect_checkbox">
Redirect HTTP to HTTPS
</label>
</div>
</div>
</div>
<!-- HTTP to HTTPS Redirect -->
<div class="row g-3 mb-3">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="backend_ssl_redirect"
name="backend_ssl_redirect">
<label class="form-check-label" for="backend_ssl_redirect">
<i class="bi bi-arrow-repeat me-1"></i>Add HTTP Redirect to HTTPS
</label>
<small class="text-muted d-block">Creates additional frontend to redirect HTTP traffic to HTTPS</small>
</div>
</div>
</div>
<div class="row g-3 mb-3 d-none" id="backend_ssl_fields">
<div class="col-md-6">
<label for="ssl_redirect_backend_name" class="form-label">Redirect Backend Name</label>
<input type="text" class="form-control" id="ssl_redirect_backend_name"
name="ssl_redirect_backend_name" placeholder="e.g. redirect">
<small class="text-muted">Name for the redirect backend</small>
</div>
<div class="col-md-6">
<label for="ssl_redirect_port" class="form-label">HTTP Redirect Port</label>
<input type="number" class="form-control" id="ssl_redirect_port"
name="ssl_redirect_port" value="80" min="1" max="65535">
<small class="text-muted">Default: 80 (leave empty for standard)</small>
</div>
</div>
<hr class="my-4">
<!-- BACKEND SECTION -->
<h6 class="text-primary mb-3"><i class="bi bi-hdd-rack me-2"></i>Backend Configuration</h6>
<div class="row g-3 mb-3">
<div class="col-md-12">
<label for="backend_name" class="form-label">Backend Name</label>
<input type="text" class="form-control" id="backend_name" name="backend_name"
placeholder="e.g. be_web" required>
</div>
</div>
<!-- Backend servers -->
<div class="mb-3">
<label class="form-label">Backend Servers</label>
<div id="backend_servers_container">
<div class="row g-3 backend-server-row">
<div class="col-md-3">
<input type="text" class="form-control" name="backend_server_names[]"
placeholder="server1" value="server1" required>
</div>
<div class="col-md-4">
<input type="text" class="form-control" name="backend_server_ips[]"
placeholder="192.168.1.10" required>
</div>
<div class="col-md-2">
<input type="number" class="form-control" name="backend_server_ports[]"
placeholder="80" min="1" max="65535" required>
</div>
<div class="col-md-2">
<input type="number" class="form-control" name="backend_server_maxconns[]"
placeholder="100">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm w-100 remove-server" style="visibility: hidden;">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-secondary btn-sm mt-2" id="add_backend_btn">
<i class="bi bi-plus-lg me-1"></i>Add Server
</button>
</div>
<!-- Health Check -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="health_check" name="health_check">
<label class="form-check-label" for="health_check">
<i class="bi bi-heart-pulse me-1"></i>Enable Health Check
</label>
</div>
</div>
<div class="col-md-12 d-none" id="health_check_fields">
<label for="health_check_link" class="form-label">Health Check Path</label>
<input type="text" class="form-control" id="health_check_link" name="health_check_link"
value="/" placeholder="/">
</div>
</div>
<div class="row g-3 mb-3" style="display: none;" id="tcp_health_check">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="health_check2" name="health_check2">
<label class="form-check-label" for="health_check2">
<i class="bi bi-heart-pulse me-1"></i>Enable TCP Health Check
</label>
</div>
</div>
</div>
<!-- Sticky Session -->
<div class="row g-3 mb-3">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sticky_session" name="sticky_session">
<label class="form-check-label" for="sticky_session">
<i class="bi bi-pin-angle me-1"></i>Sticky Session
</label>
</div>
</div>
<div class="col-md-12 d-none" id="sticky_fields">
<select class="form-select" id="sticky_session_type" name="sticky_session_type">
<option value="cookie">Cookie-based</option>
<option value="source">Source IP-based</option>
</select>
</div>
</div>
<hr class="my-4">
<!-- HEADERS & SECURITY SECTION -->
<h6 class="text-primary mb-3"><i class="bi bi-shield-lock me-2"></i>Headers & Security</h6>
<!-- Custom Headers -->
<div class="row g-3 mb-3">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="add_header" name="add_header">
<label class="form-check-label" for="add_header">
<i class="bi bi-tag me-1"></i>Add Custom Header
</label>
</div>
</div>
<div class="col-md-6 d-none" id="header_fields">
<input type="text" class="form-control" id="header_name" name="header_name"
placeholder="e.g. X-Custom-Header">
</div>
<div class="col-md-6 d-none" id="header_fields">
<input type="text" class="form-control" id="header_value" name="header_value"
placeholder="e.g. custom-value">
</div>
</div>
<!-- Server Header Removal -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="del_server_header"
name="del_server_header">
<label class="form-check-label" for="del_server_header">
<i class="bi bi-shield-lock me-1"></i>Hide Server Header
</label>
<small class="text-muted d-block">Adds: <code>http-response del-header Server</code></small>
</div>
</div>
</div>
<!-- Forward For -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="forward_for_check"
name="forward_for_check" checked>
<label class="form-check-label" for="forward_for_check">
<i class="bi bi-arrow-right me-1"></i>Forward For (X-Forwarded-For)
</label>
</div>
</div>
</div>
<hr class="my-4">
<!-- PROTECTION SECTION -->
<h6 class="text-primary mb-3"><i class="bi bi-bug me-2"></i>Protection</h6>
<!-- DOS Protection -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="add_dos" name="add_dos">
<label class="form-check-label" for="add_dos">
<i class="bi bi-exclamation-triangle me-1"></i>DOS/DDoS Protection
</label>
</div>
</div>
<div class="col-md-6 d-none" id="dos_fields">
<label for="ban_duration" class="form-label">Ban Duration</label>
<input type="text" class="form-control" id="ban_duration" name="ban_duration"
value="30m" placeholder="30m">
<small class="text-muted">e.g. 30m, 1h, 24h</small>
</div>
<div class="col-md-6 d-none" id="dos_fields">
<label for="limit_requests" class="form-label">Request Limit (per min)</label>
<input type="number" class="form-control" id="limit_requests" name="limit_requests"
value="100" min="1">
</div>
</div>
<!-- SQL Injection -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sql_injection_check"
name="sql_injection_check">
<label class="form-check-label" for="sql_injection_check">
<i class="bi bi-database-exclamation me-1"></i>SQL Injection Protection
</label>
</div>
</div>
</div>
<!-- XSS -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="xss_check" name="xss_check">
<label class="form-check-label" for="xss_check">
<i class="bi bi-code-slash me-1"></i>XSS Protection
</label>
</div>
</div>
</div>
<!-- Remote Uploads -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="remote_uploads_check"
name="remote_uploads_check">
<label class="form-check-label" for="remote_uploads_check">
<i class="bi bi-cloud-upload me-1"></i>Block Remote Uploads
</label>
</div>
</div>
</div>
<!-- Webshells -->
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="webshells_check"
name="webshells_check">
<label class="form-check-label" for="webshells_check">
<i class="bi bi-shield-exclamation me-1"></i>Block Webshells
</label>
</div>
</div>
</div>
<hr class="my-4">
<!-- CUSTOM ACL SECTION -->
<h6 class="text-primary mb-3"><i class="bi bi-shuffle me-2"></i>Custom ACL Rules (Advanced)</h6>
<div class="row g-3 mb-3 http-only">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="add_custom_acl" name="add_custom_acl">
<label class="form-check-label" for="add_custom_acl">
<i class="bi bi-sliders me-1"></i>Add Custom ACL Rule
</label>
<small class="text-muted d-block">Create additional routing or blocking rules</small>
</div>
</div>
</div>
<!-- Custom ACL Fields -->
<div class="row g-3 mb-3 http-only d-none" id="custom_acl_fields">
<div class="col-md-3">
<label for="custom_acl_name" class="form-label">ACL Name</label>
<input type="text" class="form-control" id="custom_acl_name" name="custom_acl_name"
placeholder="e.g. is_admin_path">
</div>
<div class="col-md-3">
<label for="custom_acl_type" class="form-label">Rule Type</label>
<select class="form-select" id="custom_acl_type" name="custom_acl_type">
<option value="path_beg">Path Begins With</option>
<option value="path_end">Path Ends With</option>
<option value="path_sub">Path Contains</option>
<option value="hdr">Header Contains</option>
<option value="src">Source IP</option>
<option value="method">HTTP Method</option>
</select>
</div>
<div class="col-md-3">
<label for="custom_acl_value" class="form-label">Rule Value</label>
<input type="text" class="form-control" id="custom_acl_value" name="custom_acl_value"
placeholder="e.g. /admin, api, 192.168.1.0/24">
</div>
<div class="col-md-3">
<label for="custom_acl_action" class="form-label">Action</label>
<select class="form-select" id="custom_acl_action" name="custom_acl_action">
<option value="route">Route to Backend</option>
<option value="deny">Block (Deny)</option>
<option value="redirect">Redirect</option>
</select>
</div>
<div class="col-md-6 d-none" id="acl_backend_select">
<label for="custom_acl_backend" class="form-label">Target Backend</label>
<input type="text" class="form-control" id="custom_acl_backend" name="custom_acl_backend"
placeholder="e.g. be_admin">
</div>
<div class="col-md-6 d-none" id="acl_redirect_select">
<label for="custom_acl_redirect_url" class="form-label">Redirect URL</label>
<input type="text" class="form-control" id="custom_acl_redirect_url" name="custom_acl_redirect_url"
placeholder="e.g. https://example.com/new-path">
</div>
</div>
<hr class="my-4">
<!-- SUBMIT BUTTON -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle me-2"></i>Save Configuration
</button>
</div>
</div>
</div>
</form>
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
<script src="{{ url_for('static', filename='js/form.js') }}"></script>
{% endblock %}

132
templates/login.html Normal file
View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HAProxy Configurator - Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
width: 100%;
max-width: 400px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
padding: 40px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header i {
font-size: 48px;
color: #667eea;
margin-bottom: 10px;
}
.login-header h1 {
font-size: 28px;
color: #333;
margin: 10px 0 5px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-control {
border: 1px solid #ddd;
padding: 10px 15px;
border-radius: 5px;
font-size: 14px;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-login {
width: 100%;
padding: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-weight: 600;
cursor: pointer;
margin-top: 10px;
}
.btn-login:hover {
opacity: 0.95;
}
.alert {
font-size: 14px;
margin-bottom: 20px;
}
.default-creds {
background: #f8f9fa;
border-left: 4px solid #ffc107;
padding: 10px 15px;
border-radius: 4px;
margin-top: 20px;
font-size: 12px;
}
.default-creds strong {
color: #ff6b6b;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<i class="bi bi-shield-lock"></i>
<h1>HAProxy</h1>
<p>Configurator & Manager</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.login') }}">
<div class="form-group">
<label class="form-label">Username</label>
<input type="text" class="form-control" name="username" required autofocus>
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" class="form-control" name="password" required>
</div>
<button type="submit" class="btn btn-login">
<i class="bi bi-box-arrow-in-right me-2"></i>Sign In
</button>
</form>
<div class="default-creds">
<i class="bi bi-info-circle me-1"></i>
<strong>Default credentials:</strong><br>
Username: <code>admin</code><br>
Password: <code>admin123</code>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -1,50 +1,104 @@
{% extends "base.html" %}
{% set active_page = "" %}
{% set active_page = "logs" %}
{% block title %}HAProxy • Logs{% endblock %}
{% block breadcrumb %}<nav aria-label="breadcrumb" class="mb-3"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item active" aria-current="page">Logi</li></ol></nav>{% endblock %}
{% block content %}
<h3 class="mb-4" id="status_header">Status 403 Forbidden</h3>
{% if entries %}
<div class="vstack gap-3">
{% for entry in entries %}
<div class="card">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<div><strong>Czas:</strong> {{ entry['timestamp'] }}</div>
<div><strong>IP:</strong> {{ entry['ip_address'] }}</div>
<div><strong>Metoda:</strong> {{ entry['http_method'] }}</div>
<div><strong>URL:</strong> {{ entry['requested_url'] }}</div>
<div><strong>Status:</strong> <span class="badge bg-danger">403</span></div>
</div>
<div class="col-md-6">
{% if entry['xss_alert'] %}
<p class="mb-1"><button class="btn btn-sm btn-outline-danger" data-bs-toggle="collapse" data-bs-target="#xssCollapse{{ loop.index }}"><i class="bi bi-bug"></i> XSS alert</button></p>
<div id="xssCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['xss_alert'] }}</pre></div>
{% endif %}
{% if entry['sql_alert'] %}
<p class="mb-1"><button class="btn btn-sm btn-outline-warning" data-bs-toggle="collapse" data-bs-target="#sqlCollapse{{ loop.index }}"><i class="bi bi-database-exclamation"></i> SQLi alert</button></p>
<div id="sqlCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-warning">{{ entry['sql_alert'] }}</pre></div>
{% endif %}
{% if entry['put_method'] %}
<p class="mb-1"><button class="btn btn-sm btn-outline-info" data-bs-toggle="collapse" data-bs-target="#putCollapse{{ loop.index }}"><i class="bi bi-upload"></i> PUT alert</button></p>
<div id="putCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-info">{{ entry['put_method'] }}</pre></div>
{% endif %}
{% if entry['illegal_resource'] %}
<p class="mb-1"><button class="btn btn-sm btn-outline-light" data-bs-toggle="collapse" data-bs-target="#illegalCollapse{{ loop.index }}"><i class="bi bi-shield-x"></i> Nielegalny zasób</button></p>
<div id="illegalCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-light">{{ entry['illegal_resource'] }}</pre></div>
{% endif %}
{% if entry['webshell_alert'] %}
<p class="mb-1"><button class="btn btn-sm btn-outline-danger" data-bs-toggle="collapse" data-bs-target="#webshellCollapse{{ loop.index }}"><i class="bi bi-file-earmark-code"></i> WebShell alert</button></p>
<div id="webshellCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['webshell_alert'] }}</pre></div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info"><i class="bi bi-info-circle me-1"></i>No data.</div>
{% endif %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item active" aria-current="page">Logs</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-file-earmark-text me-2"></i>HAProxy Logs</h5>
</div>
<div class="card-body">
{% if error_message %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>{{ error_message }}
</div>
{% endif %}
<!-- Controls Row -->
<div class="row g-2 mb-3">
<div class="col-md-3">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="search_filter" placeholder="Search logs...">
</div>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-outline-secondary w-100" id="clear_filter_btn" title="Clear search">
<i class="bi bi-x-circle me-1"></i>Clear
</button>
</div>
<div class="col-md-2">
<select class="form-select form-select-sm" id="logs_per_page">
<option value="25" selected>25 per page</option>
<option value="50">50 per page</option>
<option value="100">100 per page</option>
<option value="200">200 per page</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-primary w-100" id="refresh_logs_btn">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="col-md-3">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-funnel"></i></span>
<input type="text" class="form-control" id="exclude_filter" placeholder="Hide phrase (e.g. /stats)">
<button class="btn btn-outline-warning btn-sm" id="exclude_btn" type="button">Hide</button>
</div>
</div>
</div>
<!-- Statistics -->
<div class="alert alert-info small mb-3">
<i class="bi bi-info-circle me-2"></i>
<strong>Total:</strong> <span id="total_count">{{ total_logs|default(0) }}</span> logs |
<strong>Loaded:</strong> <span id="loaded_count">{{ loaded_count|default(0) }}</span> |
<strong>Displayed:</strong> <span id="match_count">0</span>
</div>
<!-- Logs Container (Dark Theme) -->
<div id="logs_container_wrapper" style="max-height: 650px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; background: #0d1117;">
<table class="table table-sm table-dark mb-0" id="logs_table">
<tbody id="logs_container">
<tr><td class="text-center text-muted py-4">Loading logs...</td></tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<small class="text-muted">
Page <span id="current_page">1</span> / <span id="total_pages">1</span>
</small>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary" id="prev_btn" disabled>
<i class="bi bi-chevron-left"></i> Prev
</button>
<button class="btn btn-outline-primary" id="next_btn">
Next <i class="bi bi-chevron-right"></i>
</button>
<button class="btn btn-outline-secondary" id="load_all_btn">
<i class="bi bi-download"></i> All
</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,115 @@
{% extends "base.html" %}
{% set active_page = "users" %}
{% block title %}HAProxy • User Management{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item active">Users</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-people me-2"></i>User Management</h5>
</div>
<div class="card-body">
<!-- New User Button -->
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#newUserModal">
<i class="bi bi-person-plus me-1"></i>Add New User
</button>
<!-- Users Table -->
<div class="table-responsive">
<table class="table table-hover" id="usersTable">
<thead class="table-light">
<tr>
<th>Username</th>
<th>Role</th>
<th>Created</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="usersList">
<tr><td colspan="5" class="text-center text-muted py-4">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- New User Modal -->
<div class="modal fade" id="newUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="newUserForm">
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" id="newUsername" required minlength="3">
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" id="newPassword" required minlength="6">
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="newIsAdmin">
<label class="form-check-label" for="newIsAdmin">
Admin privileges
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="createUserBtn">Create</button>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit User: <span id="editUsername"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editUserForm">
<div class="mb-3">
<label class="form-label">New Password (leave blank to keep current)</label>
<input type="password" class="form-control" id="editPassword" minlength="6">
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="editIsAdmin">
<label class="form-check-label" for="editIsAdmin">
Admin privileges
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="updateUserBtn">Update</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/user_manager.js') }}"></script>
{% endblock %}

123
utils/cert_manager.py Normal file
View 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
View 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
from config.settings import HAPROXY_CONFIG_PATH, HAPROXY_BACKUP_DIR, HAPROXY_STATS_PORT # ✅ TUTAJ!
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

View File

@@ -2,36 +2,88 @@ import os
HAPROXY_CFG = '/etc/haproxy/haproxy.cfg'
def is_frontend_exist(frontend_name, frontend_ip, frontend_port):
"""Check if frontend with given name, IP and port already exists"""
def sanitize_name(name):
"""Convert hostname/name to valid ACL name"""
return name.replace('.', '_').replace('-', '_').replace('/', '_').replace(':', '_')
def frontend_exists_at_port(frontend_ip, frontend_port):
"""Check if frontend already exists at specific port"""
if not os.path.exists(HAPROXY_CFG):
return None
try:
with open(HAPROXY_CFG, 'r') as f:
content = f.read()
lines = content.split('\n')
for i, line in enumerate(lines):
if line.strip().startswith('frontend'):
for j in range(i+1, min(i+10, len(lines))):
if lines[j].strip().startswith('bind'):
bind_info = lines[j].strip().split(' ', 1)[1]
bind_part = bind_info.split(' ssl ')[0].strip()
if f"{frontend_ip}:{frontend_port}" in bind_part:
return line.strip().split(' ', 1)[1] # Zwróć nazwę frontendu
elif lines[j].strip().startswith('frontend') or lines[j].strip().startswith('backend'):
break
except Exception as e:
print(f"[HAPROXY_CONFIG] Error: {e}", flush=True)
return None
def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name):
if not os.path.exists(HAPROXY_CFG):
return False
try:
with open(HAPROXY_CFG, 'r') as haproxy_cfg:
frontend_found = False
for line in haproxy_cfg:
if line.strip().startswith('frontend'):
_, existing_frontend_name = line.strip().split(' ', 1)
if existing_frontend_name.strip() == frontend_name:
frontend_found = True
else:
frontend_found = False
elif frontend_found and line.strip().startswith('bind'):
_, bind_info = line.strip().split(' ', 1)
existing_ip, existing_port = bind_info.split(':', 1)
if existing_ip.strip() == frontend_ip and existing_port.strip() == frontend_port:
return True
with open(HAPROXY_CFG, 'r') as f:
lines = f.readlines()
frontend_idx = -1
for i, line in enumerate(lines):
if 'frontend' in line and frontend_name in line:
frontend_idx = i
break
if frontend_idx == -1:
print(f"[HAPROXY_CONFIG] Frontend '{frontend_name}' not found", flush=True)
return False
for line in lines[frontend_idx:]:
if acl_name in line and 'acl' in line:
print(f"[HAPROXY_CONFIG] ACL '{acl_name}' already exists", flush=True)
return True
if line.strip().startswith('backend'):
break
insert_idx = frontend_idx + 1
for i in range(frontend_idx + 1, len(lines)):
if lines[i].strip().startswith('backend') or lines[i].strip().startswith('frontend'):
insert_idx = i
break
if 'use_backend' in lines[i] or 'default_backend' in lines[i]:
insert_idx = i + 1
# Wstaw ACL i use_backend
acl_line = f" acl {acl_name} hdr(host) -i {hostname}\n"
use_backend_line = f" use_backend {backend_name} if {acl_name}\n"
lines.insert(insert_idx, use_backend_line)
lines.insert(insert_idx, acl_line)
with open(HAPROXY_CFG, 'w') as f:
f.writelines(lines)
print(f"[HAPROXY_CONFIG] ACL '{acl_name}' added to frontend '{frontend_name}'", flush=True)
return True
except Exception as e:
print(f"[HAPROXY_CONFIG] Error checking frontend: {e}", flush=True)
return False
print(f"[HAPROXY_CONFIG] Error adding ACL: {e}", flush=True)
return False
def is_backend_exist(backend_name):
"""Check if backend with given name already exists"""
if not os.path.exists(HAPROXY_CFG):
return False
try:
with open(HAPROXY_CFG, 'r') as haproxy_cfg:
for line in haproxy_cfg:
@@ -42,25 +94,24 @@ def is_backend_exist(backend_name):
return True
except Exception as e:
print(f"[HAPROXY_CONFIG] Error checking backend: {e}", flush=True)
return False
def count_frontends_and_backends():
"""Count frontends, backends, ACLs and layer types"""
if not os.path.exists(HAPROXY_CFG):
return 0, 0, 0, 0, 0
frontend_count = 0
backend_count = 0
acl_count = 0
layer7_count = 0
layer4_count = 0
try:
with open(HAPROXY_CFG, 'r') as haproxy_cfg:
content = haproxy_cfg.read()
lines = content.split('\n')
for line in lines:
line_stripped = line.strip()
if line_stripped.startswith('frontend'):
@@ -75,100 +126,259 @@ def count_frontends_and_backends():
acl_count += 1
except Exception as e:
print(f"[HAPROXY_CONFIG] Error counting: {e}", flush=True)
return frontend_count, backend_count, acl_count, layer7_count, layer4_count
def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name,
backend_servers, health_check, health_check_tcp, health_check_link, sticky_session,
add_header, header_name, header_value, sticky_session_type, is_acl, acl_name,
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells):
# Ensure directory exists
def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name,
backend_servers, health_check, health_check_tcp, health_check_link, sticky_session,
add_header, header_name, header_value, sticky_session_type, is_acl, acl_name,
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells,
del_server_header=False, backend_ssl_redirect=False, ssl_redirect_backend_name='',
ssl_redirect_port='80', frontend_hostname='', add_custom_acl=False,
custom_acl_name='', custom_acl_type='path_beg', custom_acl_value='',
custom_acl_action='route', custom_acl_backend='', custom_acl_redirect_url=''):
os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True)
if is_backend_exist(backend_name):
return f"Backend {backend_name} already exists. Cannot add duplicate."
unique_backend_name = f"{backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else backend_name
if is_backend_exist(unique_backend_name):
return f"Backend {unique_backend_name} already exists. Cannot add duplicate."
is_no_lb = lb_method == 'no-lb'
if is_no_lb and len(backend_servers) > 1:
backend_servers = backend_servers[:1]
try:
# ===== CHECK IF FRONTEND EXISTS AT PORT =====
existing_frontend = frontend_exists_at_port(frontend_ip, frontend_port)
if existing_frontend:
print(f"[HAPROXY] Found existing frontend '{existing_frontend}' at {frontend_ip}:{frontend_port}", flush=True)
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
# ===== BACKEND =====
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
if not is_no_lb:
haproxy_cfg.write(f" balance {lb_method}\n")
if sticky_session and not is_no_lb:
if sticky_session_type == "cookie":
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
elif sticky_session_type == "source":
haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n")
haproxy_cfg.write(f" stick on src\n")
if health_check and protocol == 'http':
haproxy_cfg.write(f" option httpchk GET {health_check_link}\n")
elif health_check_tcp and protocol == 'tcp':
haproxy_cfg.write(f" option tcp-check\n")
if add_header:
haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n")
if del_server_header:
haproxy_cfg.write(f" http-response del-header Server\n")
if forward_for:
haproxy_cfg.write(f" option forwardfor\n")
# Add servers
for server_name, server_ip, server_port, maxconn in backend_servers:
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
if health_check and protocol == 'http':
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n")
else:
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"is_{unique_backend_name}"
add_acl_to_frontend(existing_frontend, acl_name_sanitized, frontend_hostname or 'localhost', unique_backend_name)
# ===== REDIRECT HTTP→HTTPS (jeśli zaznaczony) =====
if backend_ssl_redirect and ssl_redirect_backend_name:
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"{ssl_redirect_backend_name}_redirect"
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
if existing_http_frontend:
print(f"[HAPROXY] Adding redirect ACL to existing HTTP frontend '{existing_http_frontend}'", flush=True)
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n")
haproxy_cfg.write(f" mode http\n")
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
if frontend_hostname:
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name)
else:
print(f"[HAPROXY] Creating new HTTP redirect frontend at {frontend_ip}:{ssl_redirect_port}", flush=True)
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
generic_http_redirect_name = f"http_redirect_frontend"
haproxy_cfg.write(f"\nfrontend {generic_http_redirect_name}\n")
haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n")
haproxy_cfg.write(f" mode http\n")
if frontend_hostname:
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
haproxy_cfg.write(f" acl {acl_name_redirect} hdr(host) -i {frontend_hostname}\n")
haproxy_cfg.write(f" use_backend {unique_redirect_backend_name} if {acl_name_redirect}\n")
else:
haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n")
# Redirect backend
haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n")
haproxy_cfg.write(f" mode http\n")
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
return f"Backend added to existing frontend"
# ===== TWORZENIE NOWEGO FRONTENDU (GENERYCZNE NAZWY) =====
# Generuj generyczną nazwę frontendu
generic_frontend_name = f"https_frontend" if use_ssl else f"http_frontend"
print(f"[HAPROXY] Creating new frontend '{generic_frontend_name}' at {frontend_ip}:{frontend_port}", flush=True)
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
haproxy_cfg.write(f"\nfrontend {frontend_name}\n")
if is_frontend_exist(frontend_name, frontend_ip, frontend_port):
return "Frontend or Port already exists. Cannot add duplicate."
# ===== PRIMARY FRONTEND (GENERIC NAME) =====
haproxy_cfg.write(f"\nfrontend {generic_frontend_name}\n")
haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}")
if use_ssl:
haproxy_cfg.write(f" ssl crt {ssl_cert_path}")
haproxy_cfg.write("\n")
if https_redirect:
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
if forward_for:
haproxy_cfg.write(f" option forwardfor\n")
# Headers zaraz po BIND/CERT
haproxy_cfg.write(f" http-request set-header X-Forwarded-For %[src]\n")
if use_ssl:
haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto https\n")
else:
haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto http\n")
haproxy_cfg.write(f" mode {protocol}\n")
haproxy_cfg.write(f" balance {lb_method}\n")
# Add protection rules
# ACL dla pierwszego vhost
acl_name_sanitized = None
if frontend_hostname:
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}"
haproxy_cfg.write(f" acl {acl_name_sanitized} hdr(host) -i {frontend_hostname}\n")
if not is_no_lb:
haproxy_cfg.write(f" balance {lb_method}\n")
if forward_for:
haproxy_cfg.write(f" option forwardfor\n")
# Protections
if is_dos:
haproxy_cfg.write(f" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n")
haproxy_cfg.write(f" http-request track-sc0 src\n")
haproxy_cfg.write(f" acl abuse sc_http_req_rate(0) gt {limit_requests}\n")
haproxy_cfg.write(f" http-request silent-drop if abuse\n")
if sql_injection_check:
# POPRAWNE escape sequence'i - podwójny backslash dla haproxy
haproxy_cfg.write(" acl is_sql_injection urlp_reg -i (union|select|insert|update|delete|drop|@@|1=1|`1)\n")
haproxy_cfg.write(" acl is_long_uri path_len gt 400\n")
haproxy_cfg.write(" acl semicolon_path path_reg -i ^.*;.*\n")
haproxy_cfg.write(" acl is_sql_injection2 urlp_reg -i (;|substring|extract|union\\s+all|order\\s+by)\\s+(\\d+|--\\+)\n")
haproxy_cfg.write(f" http-request deny if is_sql_injection or is_long_uri or semicolon_path or is_sql_injection2\n")
if is_xss:
haproxy_cfg.write(" acl is_xss_attack urlp_reg -i (<|>|script|alert|onerror|onload|javascript)\n")
haproxy_cfg.write(" acl is_xss_attack_2 urlp_reg -i (<\\s*script\\s*|javascript:|<\\s*img\\s*src\\s*=|<\\s*a\\s*href\\s*=|<\\s*iframe\\s*src\\s*=|\\bon\\w+\\s*=|<\\s*input\\s*[^>]*\\s*value\\s*=|<\\s*form\\s*action\\s*=|<\\s*svg\\s*on\\w+\\s*=)\n")
haproxy_cfg.write(" acl is_xss_attack_hdr hdr_reg(Cookie|Referer|User-Agent) -i (<|>|script|alert|onerror|onload|javascript)\n")
haproxy_cfg.write(f" http-request deny if is_xss_attack or is_xss_attack_2 or is_xss_attack_hdr\n")
if is_webshells:
haproxy_cfg.write(" acl blocked_webshell path_reg -i /(cmd|shell|backdoor|webshell|phpspy|c99|kacak|b374k|log4j|log4shell|wsos|madspot|malicious|evil).*\\.php.*\n")
haproxy_cfg.write(f" http-request deny if blocked_webshell\n")
haproxy_cfg.write(f" default_backend {backend_name}\n")
# Backend section
haproxy_cfg.write(f"\nbackend {backend_name}\n")
haproxy_cfg.write(f" balance {lb_method}\n")
if sticky_session:
if https_redirect:
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
if del_server_header:
haproxy_cfg.write(f" http-response del-header Server\n")
# Backend routing
if acl_name_sanitized:
haproxy_cfg.write(f" use_backend {unique_backend_name} if {acl_name_sanitized}\n")
else:
haproxy_cfg.write(f" default_backend {unique_backend_name}\n")
# ===== BACKEND =====
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
if not is_no_lb:
haproxy_cfg.write(f" balance {lb_method}\n")
if sticky_session and not is_no_lb:
if sticky_session_type == "cookie":
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
elif sticky_session_type == "source":
haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n")
haproxy_cfg.write(f" stick on src\n")
if health_check and protocol == 'http':
haproxy_cfg.write(f" option httpchk GET {health_check_link}\n")
elif health_check_tcp and protocol == 'tcp':
haproxy_cfg.write(f" option tcp-check\n")
if add_header:
haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n")
# Add backend servers
if del_server_header:
haproxy_cfg.write(f" http-response del-header Server\n")
if forward_for:
haproxy_cfg.write(f" option forwardfor\n")
for server_name, server_ip, server_port, maxconn in backend_servers:
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
if health_check and protocol == 'http':
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n")
else:
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
return "Configuration updated successfully!"
# ===== REDIRECT HTTP -> HTTPS (GENERIC NAME) =====
if backend_ssl_redirect and ssl_redirect_backend_name:
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"{ssl_redirect_backend_name}_redirect"
# Check if HTTP frontend exists
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
if not existing_http_frontend:
generic_http_redirect_name = f"http_redirect_frontend"
haproxy_cfg.write(f"\nfrontend {generic_http_redirect_name}\n")
haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n")
haproxy_cfg.write(f" mode http\n")
if frontend_hostname:
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
haproxy_cfg.write(f" acl {acl_name_redirect} hdr(host) -i {frontend_hostname}\n")
haproxy_cfg.write(f" use_backend {unique_redirect_backend_name} if {acl_name_redirect}\n")
else:
haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n")
else:
if frontend_hostname:
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name)
# Redirect backend
haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n")
haproxy_cfg.write(f" mode http\n")
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
return "Configuration updated successfully!"
except Exception as e:
print(f"[HAPROXY_CONFIG] Error updating config: {e}", flush=True)
return f"Error: {e}"

View File

@@ -1,7 +1,7 @@
import requests
import csv
HAPROXY_STATS_URL = 'http://127.0.0.1:8484/;csv'
HAPROXY_STATS_URL = 'http://127.0.0.1:8404/stats;csv'
def fetch_haproxy_stats():
try:
@@ -13,18 +13,58 @@ def fetch_haproxy_stats():
def parse_haproxy_stats(stats_data):
data = []
header_row = stats_data.splitlines()[0].replace('# ', '')
reader = csv.DictReader(stats_data.splitlines(), fieldnames=header_row.split(','))
lines = [line for line in stats_data.splitlines() if line.strip()]
if not lines:
return data
header_row = lines[0].replace('# ', '')
# Parse CSV
reader = csv.DictReader(lines, fieldnames=header_row.split(','))
next(reader)
for row in reader:
if row['svname'] != 'BACKEND':
data.append({
'frontend_name': row['pxname'],
'server_name': row['svname'],
'4xx_errors': row['hrsp_4xx'],
'5xx_errors': row['hrsp_5xx'],
'bytes_in_mb': f'{float(row["bin"]) / (1024 * 1024):.2f}',
'bytes_out_mb': f'{float(row["bout"]) / (1024 * 1024):.2f}',
'conn_tot': row['conn_tot'],
})
return data
if row.get('svname') == 'BACKEND':
continue
row = {k: v.strip() if isinstance(v, str) else v for k, v in row.items()}
try:
conn_tot = int(row.get('conn_tot', 0) or 0)
except (ValueError, TypeError):
conn_tot = 0
try:
hrsp_4xx = int(row.get('hrsp_4xx', 0) or 0)
except (ValueError, TypeError):
hrsp_4xx = 0
try:
hrsp_5xx = int(row.get('hrsp_5xx', 0) or 0)
except (ValueError, TypeError):
hrsp_5xx = 0
try:
bin_bytes = float(row.get('bin', 0) or 0)
bytes_in_mb = bin_bytes / (1024 * 1024)
except (ValueError, TypeError):
bytes_in_mb = 0.0
try:
bout_bytes = float(row.get('bout', 0) or 0)
bytes_out_mb = bout_bytes / (1024 * 1024)
except (ValueError, TypeError):
bytes_out_mb = 0.0
data.append({
'frontend_name': row.get('pxname', 'Unknown'),
'server_name': row.get('svname', 'Unknown'),
'4xx_errors': hrsp_4xx,
'5xx_errors': hrsp_5xx,
'bytes_in_mb': bytes_in_mb,
'bytes_out_mb': bytes_out_mb,
'conn_tot': conn_tot,
})
return data