new_functions_and_fixes #1

Merged
gru merged 33 commits from new_functions_and_fixes into master 2025-11-03 14:35:20 +01:00
18 changed files with 1995 additions and 684 deletions

46
app.py
View File

@@ -1,15 +1,16 @@
from flask import Flask, render_template, render_template_string
import configparser
import os
import sys
import ssl
import configparser
from flask import Flask, render_template, render_template_string
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
from utils.haproxy_config import update_haproxy_config, count_frontends_and_backends
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
@@ -73,23 +74,23 @@ try:
certificate_path = config2.get('ssl', 'certificate_path')
private_key_path = config2.get('ssl', 'private_key_path')
else:
print(f"[APP] No [ssl] section in {SSL_INI}", flush=True)
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)
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)
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] SSL context loaded", flush=True)
except Exception as e:
print(f"[APP] SSL error: {e}", flush=True)
print(f"[APP] SSL error: {e}", flush=True)
sys.exit(1)
@@ -99,11 +100,28 @@ def display_haproxy_stats():
parsed_stats = parse_haproxy_stats(haproxy_stats)
return render_template('statistics.html', stats=parsed_stats)
@app.route('/logs')
def display_logs():
@app.route('/logs', endpoint='display_logs')
#@requires_auth
def display_haproxy_logs():
log_file_path = '/var/log/haproxy.log'
parsed_entries = parse_log_file(log_file_path)
return render_template('logs.html', entries=parsed_entries)
if not os.path.exists(log_file_path):
return render_template('logs.html',
logs=[],
error_message=f"Log file not found: {log_file_path}")
try:
logs = parse_log_file(log_file_path)
if not logs:
return render_template('logs.html',
logs=[],
error_message="Log file is empty or unreadable")
return render_template('logs.html', logs=logs)
except Exception as e:
return render_template('logs.html',
logs=[],
error_message=f"Error parsing logs: {str(e)}")
@app.route('/home')

View File

@@ -1,7 +1,13 @@
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 +18,118 @@ 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)
# 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)
with open(log_file_path, 'r') as log_file:
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.'
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 = ''
try:
# Extract syslog header
syslog_match = re.search(
r'<\d+>(\w+\s+\d+\s+\d+:\d+:\d+).*haproxy\[\d+\]:\s+',
line
)
if "admin" in line:
illegal_resource = 'Possible Illegal Resource Access Attempt Was Made.'
else:
illegal_resource = ''
if not syslog_match:
continue
if combined_webshells_pattern.search(line):
webshell_alert = 'Possible WebShell Attack Attempt Was Made.'
else:
webshell_alert = ''
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
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:
continue
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'
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
'webshell_alert': webshell_alert,
})
except Exception as e:
print(f"Error parsing line: {e}")
continue
except FileNotFoundError:
print(f"Log file not found: {log_file_path}")
return []
except Exception as e:
print(f"Error reading log file: {e}")
return []
return parsed_entries

View File

@@ -8,12 +8,17 @@ edit_bp = Blueprint('edit', __name__)
@requires_auth
def edit_haproxy_config():
if request.method == 'POST':
edited_config = request.form['haproxy_config']
edited_config = request.form.get('haproxy_config', '')
action = request.form.get('action', 'check')
print(f"[EDIT] POST action: {action}", flush=True)
try:
with open('/etc/haproxy/haproxy.cfg', 'w') as f:
f.write(edited_config)
print(f"[EDIT] Configuration saved successfully", flush=True)
except Exception as e:
print(f"[EDIT] Error writing config: {e}", flush=True)
return render_template(
'edit.html',
config_content=edited_config,
@@ -21,57 +26,80 @@ def edit_haproxy_config():
check_level="danger"
)
def run_check():
check_output = ""
check_level = "success"
try:
result = subprocess.run(
['haproxy', '-c', '-V', '-f', '/etc/haproxy/haproxy.cfg'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
text=True,
timeout=10
)
out = (result.stdout or '').strip()
check_output = (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 = ""
if not check_output:
check_output = "Configuration file is valid"
check_level = "success"
if 'save_check' in request.form:
_, check_output, check_level = run_check()
if "Warning" in check_output or "Warnings" in check_output:
check_level = "warning"
check_output = f"{check_output}"
else:
check_output = f"{check_output}"
elif 'save_reload' in request.form:
rc, out, level = run_check()
check_output, check_level = out, level
print(f"[EDIT] Config validation: SUCCESS", flush=True)
else:
if not check_output:
check_output = f"Check failed with return code {result.returncode}"
check_output = f"{check_output}"
check_level = "danger"
print(f"[EDIT] Config validation: FAILED - {check_output}", flush=True)
if rc == 0:
except subprocess.TimeoutExpired:
check_output = "✗ Configuration check timed out"
check_level = "danger"
print(f"[EDIT] Config validation: TIMEOUT", flush=True)
except Exception as e:
check_output = f"✗ Error checking config: {e}"
check_level = "danger"
print(f"[EDIT] Config validation ERROR: {e}", flush=True)
if action == "save" and check_level == "success":
print(f"[EDIT] Attempting HAProxy restart...", flush=True)
try:
supervisor_result = subprocess.run(
restart_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}"
if restart_result.returncode == 0 or 'No such process' in restart_result.stdout:
check_output += "\n\n✓ HAProxy restart signal sent successfully"
check_output += "\n(supervisord will restart the process)"
print(f"[EDIT] HAProxy restart successful", flush=True)
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_output += f"\n\n⚠ Restart returned code {restart_result.returncode}"
if restart_result.stdout:
check_output += f"\nOutput: {restart_result.stdout}"
check_level = "warning"
print(f"[EDIT] Restart warning: {restart_result.stdout}", flush=True)
except subprocess.TimeoutExpired:
check_output += "\n\n⚠ Restart command timed out"
check_level = "warning"
print(f"[EDIT] Restart TIMEOUT", flush=True)
except Exception as e:
check_output += f"\n\n⚠ Restart error: {e}"
check_level = "warning"
print(f"[EDIT] Restart ERROR: {e}", flush=True)
print(f"[EDIT] Returning check_level={check_level}, output length={len(check_output)}", flush=True)
return render_template(
'edit.html',
@@ -80,12 +108,19 @@ def edit_haproxy_config():
check_level=check_level
)
# GET request - load current config
try:
with open('/etc/haproxy/haproxy.cfg', 'r') as f:
config_content = f.read()
print(f"[EDIT] Config loaded successfully ({len(config_content)} bytes)", flush=True)
except FileNotFoundError:
config_content = "# HAProxy configuration file not found\n# Please create /etc/haproxy/haproxy.cfg"
config_content = "# HAProxy configuration file not found\n# Please create /etc/haproxy/haproxy.cfg\n"
print(f"[EDIT] Config file not found", flush=True)
except PermissionError:
config_content = "# Permission denied reading HAProxy configuration file"
config_content = "# Permission denied reading HAProxy configuration file\n"
print(f"[EDIT] Permission denied reading config", flush=True)
except Exception as e:
config_content = f"# Error reading config: {e}\n"
print(f"[EDIT] Error reading config: {e}", flush=True)
return render_template('edit.html', config_content=config_content)

View File

@@ -1,104 +1,252 @@
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
import subprocess
from auth.auth_middleware import requires_auth
from utils.haproxy_config import update_haproxy_config, count_frontends_and_backends
main_bp = Blueprint('main', __name__)
def reload_haproxy():
"""Reload HAProxy by killing it - supervisord restarts automatically"""
try:
# Validate config first
result = subprocess.run(
['haproxy', '-c', '-V', '-f', '/etc/haproxy/haproxy.cfg'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=10
)
if result.returncode != 0:
return False, f"Config validation failed: {result.stdout}"
# Kill haproxy - supervisord will restart it automatically
result = subprocess.run(
['pkill', '-f', 'haproxy'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=10
)
if result.returncode == 0 or 'No such process' in result.stdout:
print("[HAPROXY] Process killed, supervisord will restart", flush=True)
return True, "HAProxy restarted successfully"
else:
print(f"[HAPROXY] pkill failed: {result.stdout}", flush=True)
return False, f"pkill failed: {result.stdout}"
except Exception as e:
print(f"[HAPROXY] Error: {e}", flush=True)
return False, f"Error: {str(e)}"
@main_bp.route('/', methods=['GET', 'POST'])
@requires_auth
def index():
if request.method == 'POST':
frontend_name = request.form['frontend_name']
# Frontend IP i port
frontend_ip = request.form['frontend_ip']
frontend_port = request.form['frontend_port']
frontend_hostname = request.form.get('frontend_hostname', '').strip()
lb_method = request.form['lb_method']
protocol = request.form['protocol']
backend_name = request.form['backend_name']
# Header options
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 ''
# Server header removal
del_server_header = 'del_server_header' in request.form
# Get all backend servers data
# Backend SSL redirect
backend_ssl_redirect = 'backend_ssl_redirect' in request.form
ssl_redirect_backend_name = request.form.get('ssl_redirect_backend_name', '').strip() if backend_ssl_redirect else ''
ssl_redirect_port = request.form.get('ssl_redirect_port', '80')
# Backend servers
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 ''
# Custom ACL
add_custom_acl = 'add_custom_acl' in request.form
custom_acl_name = request.form.get('custom_acl_name', '').strip() if add_custom_acl else ''
custom_acl_type = request.form.get('custom_acl_type', 'path_beg') if add_custom_acl else ''
custom_acl_value = request.form.get('custom_acl_value', '').strip() if add_custom_acl else ''
custom_acl_action = request.form.get('custom_acl_action', 'route') if add_custom_acl else ''
custom_acl_backend = request.form.get('custom_acl_backend', '').strip() if add_custom_acl else ''
custom_acl_redirect_url = request.form.get('custom_acl_redirect_url', '').strip() if add_custom_acl else ''
# SSL
use_ssl = 'ssl_checkbox' in request.form
ssl_cert_path = request.form['ssl_cert_path']
ssl_cert_path = request.form.get('ssl_cert_path', '/app/ssl/haproxy-configurator.pem')
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"]
# DOS Protection
is_dos = 'add_dos' in request.form
ban_duration = request.form.get('ban_duration', '30m')
limit_requests = request.form.get('limit_requests', '100')
# Forward For
forward_for = 'forward_for_check' in request.form
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
sql_injection_check = 'sql_injection_check' in request.form
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 ''
# XSS
is_xss = 'xss_check' in request.form
# Remote uploads
is_remote_upload = 'remote_uploads_check' in request.form
# Webshells
is_webshells = 'webshells_check' in request.form
# Path-based redirects (legacy)
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 ''
redirect_domain_name = request.form.get('redirect_domain_name', '')
root_redirect = request.form.get('root_redirect', '')
redirect_to = request.form.get('redirect_to', '')
# Combine backend server info into a list of tuples (name, ip, port, maxconns)
# Forbidden paths (legacy)
is_forbidden_path = 'add_acl_path' in request.form
forbidden_name = request.form.get('forbidden_name', '')
allowed_ip = request.form.get('allowed_ip', '')
forbidden_path = request.form.get('forbidden_path', '')
# Build backend_servers list
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
if 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 checks
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_link = request.form.get('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
sticky_session = False
sticky_session_type = ""
if 'sticky_session' in request.form:
sticky_session = True
sticky_session_type = request.form['sticky_session_type']
sticky_session_type = request.form.get('sticky_session_type', 'cookie')
# Update the HAProxy config file
# Legacy ACL (unused, kept for compatibility)
is_acl = False
acl_name = ''
acl_action = ''
acl_backend_name = ''
# Frontend name (None - will be generated)
frontend_name = None
# Call update_haproxy_config
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
frontend_name=frontend_name,
frontend_ip=frontend_ip,
frontend_port=frontend_port,
lb_method=lb_method,
protocol=protocol,
backend_name=backend_name,
backend_servers=backend_servers,
health_check=health_check,
health_check_tcp=health_check_tcp,
health_check_link=health_check_link,
sticky_session=sticky_session,
add_header=add_header,
header_name=header_name,
header_value=header_value,
sticky_session_type=sticky_session_type,
is_acl=is_acl,
acl_name=acl_name,
acl_action=acl_action,
acl_backend_name=acl_backend_name,
use_ssl=use_ssl,
ssl_cert_path=ssl_cert_path,
https_redirect=https_redirect,
is_dos=is_dos,
ban_duration=ban_duration,
limit_requests=limit_requests,
forward_for=forward_for,
is_forbidden_path=is_forbidden_path,
forbidden_name=forbidden_name,
allowed_ip=allowed_ip,
forbidden_path=forbidden_path,
sql_injection_check=sql_injection_check,
is_xss=is_xss,
is_remote_upload=is_remote_upload,
add_path_based=add_path_based,
redirect_domain_name=redirect_domain_name,
root_redirect=root_redirect,
redirect_to=redirect_to,
is_webshells=is_webshells,
del_server_header=del_server_header,
backend_ssl_redirect=backend_ssl_redirect,
ssl_redirect_backend_name=ssl_redirect_backend_name,
ssl_redirect_port=ssl_redirect_port,
frontend_hostname=frontend_hostname,
add_custom_acl=add_custom_acl,
custom_acl_name=custom_acl_name,
custom_acl_type=custom_acl_type,
custom_acl_value=custom_acl_value,
custom_acl_action=custom_acl_action,
custom_acl_backend=custom_acl_backend,
custom_acl_redirect_url=custom_acl_redirect_url
)
return render_template('index.html', message=message)
return render_template('index.html')
# ===== DETERMINE MESSAGE TYPE =====
message_type = "success" # Default
# Check for ERROR conditions
if "error" in message.lower():
message_type = "danger"
elif "failed" in message.lower():
message_type = "danger"
elif "already exists" in message.lower():
message_type = "danger"
elif "cannot add" in message.lower():
message_type = "danger"
# SUCCESS conditions
elif "configuration updated successfully" in message.lower():
message_type = "success"
elif "backend added to existing" in message.lower():
message_type = "success"
# ===== RELOAD HAPROXY (JEŚLI SUCCESS) =====
if message_type == "success":
reload_ok, reload_msg = reload_haproxy()
if reload_ok:
message = message + "" + reload_msg
message_type = "success"
else:
message = message + "" + reload_msg
message_type = "warning"
return render_template('index.html',
message=message,
message_type=message_type)
# GET request - display stats
frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends()
return render_template('index.html',
frontend_count=frontend_count,
backend_count=backend_count,
acl_count=acl_count,
layer7_count=layer7_count,
layer4_count=layer4_count)

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;
}
}

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,23 +1,22 @@
(() => {
'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
// ===== 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
// ===== DOS PROTECTION =====
const dosCheckbox = $('#add_dos');
const dosFields = $('#dos_fields');
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
// HTTP only groups
// ===== PROTOCOL CHANGE (HTTP/TCP) =====
const protocolSelect = $('#protocol');
const httpGroups = $$('.http-only, #forbidden_acl_container');
const httpToggles = [
@@ -28,78 +27,56 @@
$('#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) {
// hide optional groups if protocol != http
[forbiddenFields, pathFields].forEach(el => el && toggle(false, el));
httpToggles.forEach(input => { if (input) input.checked = false; });
[forbiddenFields, pathFields].forEach(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));
// ===== BACKEND SSL REDIRECT =====
const backendSslCheckbox = $('#backend_ssl_redirect');
const backendSslFields = $('#backend_ssl_fields');
// 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.');
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,103 @@
document.addEventListener('DOMContentLoaded', function() {
const filterIp = document.getElementById('filter_ip');
const filterStatus = document.getElementById('filter_status');
const filterMethod = document.getElementById('filter_method');
const filterThreats = document.getElementById('filter_threats');
const filterHideStats = document.getElementById('filter_hide_stats');
const resetBtn = document.getElementById('reset_filters');
const logsTable = document.getElementById('logs_table');
if (!logsTable) return; // Exit if no logs
const allRows = Array.from(document.querySelectorAll('.log-row'));
// Filter function
function applyFilters() {
const ipValue = filterIp.value.toLowerCase();
const statusValue = filterStatus.value;
const methodValue = filterMethod.value;
const showThreats = filterThreats.checked;
const hideStats = filterHideStats.checked;
let visibleCount = 0;
let threatCount = 0;
let count2xx = 0, count4xx = 0, count5xx = 0;
const uniqueIps = new Set();
allRows.forEach(row => {
const ip = row.dataset.ip;
const status = row.dataset.status;
const method = row.dataset.method;
const hasThreat = row.dataset.threats === '1';
const url = row.querySelector('td:nth-child(4)').textContent.trim();
let show = true;
// IP filter
if (ipValue && !ip.includes(ipValue)) {
show = false;
}
// Status filter
if (statusValue) {
const statusStart = statusValue;
if (!status.startsWith(statusStart)) {
show = false;
}
}
// Method filter
if (methodValue && method !== methodValue) {
show = false;
}
// Threats filter
if (!showThreats && hasThreat) {
show = false;
}
// Hide /stats filter
if (hideStats && url.includes('/stats')) {
show = false;
}
row.style.display = show ? '' : 'none';
if (show) {
visibleCount++;
if (hasThreat) threatCount++;
if (status.startsWith('2')) count2xx++;
if (status.startsWith('4')) count4xx++;
if (status.startsWith('5')) count5xx++;
uniqueIps.add(ip);
}
});
// Update stats
document.getElementById('stat_total').textContent = visibleCount;
document.getElementById('stat_threats').textContent = threatCount;
document.getElementById('stat_2xx').textContent = count2xx;
document.getElementById('stat_4xx').textContent = count4xx;
document.getElementById('stat_5xx').textContent = count5xx;
document.getElementById('stat_ips').textContent = uniqueIps.size;
}
// Event listeners
filterIp.addEventListener('input', applyFilters);
filterStatus.addEventListener('change', applyFilters);
filterMethod.addEventListener('change', applyFilters);
filterThreats.addEventListener('change', applyFilters);
filterHideStats.addEventListener('change', applyFilters);
// Reset button
resetBtn.addEventListener('click', function() {
filterIp.value = '';
filterStatus.value = '';
filterMethod.value = '';
filterThreats.checked = true;
filterHideStats.checked = true;
applyFilters();
});
applyFilters();
});

View File

@@ -5,6 +5,16 @@ loglevel=info
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[program:haproxy]
command=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg
autostart=true
@@ -14,6 +24,8 @@ stdout_logfile=/var/log/haproxy.log
priority=100
stopasgroup=true
killasgroup=true
startsecs=10
stopwaitsecs=10
[program:flask_app]
command=python /app/app.py
@@ -26,10 +38,3 @@ priority=999
environment=FLASK_APP=/app/app.py,FLASK_ENV=production,PYTHONUNBUFFERED=1
startsecs=10
stopasgroup=true
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

Binary file not shown.

View File

@@ -7,6 +7,7 @@
<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">

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 %}
{% 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 %}
<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>
<!-- 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('success') }}" role="alert">
<pre class="mb-0">{{ check_output }}</pre>
<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>
{% 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>
{% endblock %}
{% block page_js %}
<script src="{{ url_for('static', filename='js/edit.js') }}"></script>
<!-- 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,467 @@
{% 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="row mb-4">
<div class="col-md-3">
<div class="card text-center 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>
<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>
</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>
</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>
</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>
</div>
</div>
{% if message %}
<div class="alert {% if 'already exists' in message %}alert-danger{% else %}alert-success{% endif %} alert-dismissible" role="alert">
<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{% else %}info-circle{% endif %} me-2"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row g-3">
<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 class="form-label" for="frontend_name">Name</label>
<input type="text" class="form-control" name="frontend_name" id="frontend_name" required>
<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 class="form-label" for="frontend_ip">IP</label>
<input type="text" class="form-control" name="frontend_ip" id="frontend_ip" required>
<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-3">
<label class="form-label" for="frontend_port">Port</label>
<input type="number" class="form-control" name="frontend_port" id="frontend_port" required>
<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="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="row g-3 mb-3">
<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">
<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 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">
<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="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>
<option value="source">Source IP Hash</option>
<option value="uri">URI Hash</option>
<option value="static-rr">Static Round Robin (WRR)</option>
<option value="no-lb">No Load Balancing (single host)</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>
<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 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 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 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>
<!-- 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="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 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="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 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 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>
<!-- Backend SSL 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 on port 80</small>
</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 class="row g-3 mb-3 d-none" id="backend_ssl_fields">
<div class="col-md-12">
<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">
</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>
<!-- 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>
<div id="backend_servers_container" class="mt-3">
<!-- 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">
<label class="form-label" for="name1">Server name</label>
<input type="text" id="name1" class="form-control" name="backend_server_names[]" placeholder="server1" required>
<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 class="form-label" for="ip1">IP</label>
<input type="text" id="ip1" class="form-control" name="backend_server_ips[]" required>
<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 class="form-label" for="port1">Port</label>
<input type="number" id="port1" class="form-control" name="backend_server_ports[]" required>
<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 class="form-label" for="maxconn1">MaxConn</label>
<input type="number" id="maxconn1" class="form-control" name="backend_server_maxconns[]">
<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 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>
{% endblock %}
{% block page_js %}
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
<script src="{{ url_for('static', filename='js/form.js') }}"></script>
{% endblock %}

View File

@@ -1,50 +1,211 @@
{% 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 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">Access Logs</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 class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-file-text me-2"></i>HAProxy Access Logs</h5>
</div>
<div class="col-md-6">
<div class="card-body">
{% if error_message %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Warning:</strong> {{ error_message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if logs and logs|length > 0 %}
<div class="row mb-3 g-2">
<div class="col-auto">
<input type="text" class="form-control form-control-sm" id="filter_ip" placeholder="Filter by IP">
</div>
<div class="col-auto">
<select class="form-select form-select-sm" id="filter_status" style="width: auto;">
<option value="">All Status</option>
<option value="2">2xx (Success)</option>
<option value="3">3xx (Redirect)</option>
<option value="4">4xx (Client Error)</option>
<option value="5">5xx (Server Error)</option>
</select>
</div>
<div class="col-auto">
<select class="form-select form-select-sm" id="filter_method" style="width: auto;">
<option value="">All Methods</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="filter_threats" checked>
<label class="form-check-label" for="filter_threats" style="margin-top: 5px;">
Show Threats
</label>
</div>
</div>
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="filter_hide_stats" checked>
<label class="form-check-label" for="filter_hide_stats" style="margin-top: 5px;">
Hide /stats
</label>
</div>
</div>
<div class="col-auto ms-auto">
<button class="btn btn-sm btn-secondary" id="reset_filters">Reset</button>
</div>
</div>
<div class="row mb-3 g-2">
<div class="col-md-2">
<div class="card text-center" style="font-size: 0.9rem;">
<div class="card-body p-2">
<div class="text-muted small">Total</div>
<strong id="stat_total">{{ logs|length }}</strong>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card text-center text-danger" style="font-size: 0.9rem;">
<div class="card-body p-2">
<div class="text-muted small">Threats</div>
<strong id="stat_threats">0</strong>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card text-center text-success" style="font-size: 0.9rem;">
<div class="card-body p-2">
<div class="text-muted small">2xx</div>
<strong id="stat_2xx">0</strong>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card text-center text-warning" style="font-size: 0.9rem;">
<div class="card-body p-2">
<div class="text-muted small">4xx</div>
<strong id="stat_4xx">0</strong>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card text-center text-danger" style="font-size: 0.9rem;">
<div class="card-body p-2">
<div class="text-muted small">5xx</div>
<strong id="stat_5xx">0</strong>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card text-center" style="font-size: 0.9rem;">
<div class="card-body p-2">
<div class="text-muted small">Unique IPs</div>
<strong id="stat_ips">0</strong>
</div>
</div>
</div>
</div>
<hr>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Timestamp</th>
<th>IP Address</th>
<th>HTTP Method</th>
<th>Requested URL</th>
<th>Status Code</th>
<th>Alerts</th>
</tr>
</thead>
<tbody id="logs_table">
{% for entry in logs %}
<tr class="log-row"
data-ip="{{ entry['ip_address'] }}"
data-status="{{ entry['status_code'] }}"
data-method="{{ entry['http_method'] }}"
data-threats="{% if entry['xss_alert'] or entry['sql_alert'] or entry['put_method'] or entry['webshell_alert'] or entry['illegal_resource'] %}1{% else %}0{% endif %}">
<td>{{ entry['timestamp'] }}</td>
<td>
<span class="badge bg-secondary">{{ entry['ip_address'] }}</span>
</td>
<td>
<span class="badge bg-primary">{{ entry['http_method'] }}</span>
</td>
<td class="text-truncate" style="max-width: 300px;" title="{{ entry['requested_url'] }}">
{{ entry['requested_url'] }}
</td>
<td>
<span class="badge {% if entry['status_code']|int >= 200 and entry['status_code']|int < 300 %}bg-success{% elif entry['status_code']|int >= 300 and entry['status_code']|int < 400 %}bg-secondary{% elif entry['status_code']|int >= 400 and entry['status_code']|int < 500 %}bg-warning{% else %}bg-danger{% endif %}">
{{ entry['status_code'] }}
</span>
</td>
<td>
{% 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>
<span class="badge bg-danger">XSS</span>
{% 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>
<span class="badge bg-danger">SQL</span>
{% 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>
<span class="badge bg-warning">PUT</span>
{% 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>
<span class="badge bg-danger">Webshell</span>
{% endif %}
</div>
</div>
</div>
</div>
{% if entry['illegal_resource'] %}
<span class="badge bg-warning">403</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif logs %}
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>No log entries match your filters.
</div>
{% else %}
<div class="alert alert-info"><i class="bi bi-info-circle me-1"></i>No data.</div>
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading"><i class="bi bi-exclamation-circle me-2"></i>No logs available</h4>
<hr>
<p class="mb-2"><strong>Possible reasons:</strong></p>
<ul class="mb-0">
<li>Log file does not exist or is not readable</li>
<li>HAProxy is not configured to log requests</li>
<li>Log file path is incorrect in configuration</li>
<li>No requests have been processed yet</li>
</ul>
<hr class="my-2">
<p class="small text-muted mb-0">Check HAProxy configuration and log file permissions.</p>
</div>
{% endif %}
</div>
</div>
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
{% endblock %}

View File

@@ -2,33 +2,86 @@ 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'):
# Szukaj bind line
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]
if f"{frontend_ip}:{frontend_port}" in bind_info:
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):
"""Dodaj ACL i use_backend do istniejącego frontendu"""
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:
with open(HAPROXY_CFG, 'r') as f:
lines = f.readlines()
# Znajdź frontend
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:
return False
# Sprawdź czy ACL już istnieje
for line in lines[frontend_idx:]:
if acl_name in line and 'acl' in line:
return True # Już istnieje
if line.strip().startswith('backend'):
break
# Znajdź ostatnią linię ACL/use_backend w tym frontendzie
insert_idx = frontend_idx + 1
for i in range(frontend_idx + 1, len(lines)):
if lines[i].strip().startswith('backend'):
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)
return True
except Exception as e:
print(f"[HAPROXY_CONFIG] Error checking frontend: {e}", flush=True)
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
@@ -46,7 +99,6 @@ def is_backend_exist(backend_name):
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
@@ -84,37 +136,112 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
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):
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=''):
# Ensure directory exists
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:
# Frontend już istnieje - dodaj tylko backend + ACL
print(f"[HAPROXY] Found existing frontend '{existing_frontend}' at {frontend_ip}:{frontend_port}", flush=True)
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
haproxy_cfg.write(f"\nfrontend {frontend_name}\n")
# ===== BACKEND =====
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
if is_frontend_exist(frontend_name, frontend_ip, frontend_port):
return "Frontend or Port already exists. Cannot add duplicate."
if not is_no_lb:
haproxy_cfg.write(f" balance {lb_method}\n")
haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}")
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 use_ssl:
haproxy_cfg.write(f" ssl crt {ssl_cert_path}")
haproxy_cfg.write("\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 https_redirect:
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\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")
haproxy_cfg.write(f" mode {protocol}\n")
haproxy_cfg.write(f" balance {lb_method}\n")
# Add servers
for server_name, server_ip, server_port, maxconn in backend_servers:
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
# Add protection rules
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")
# Dodaj ACL do istniejącego frontendu
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)
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"
generic_http_redirect_name = f"http_redirect_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:
# ===== 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")
# 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")
# 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")
@@ -122,7 +249,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
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")
@@ -139,13 +265,25 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
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")
if https_redirect:
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
# Backend section
haproxy_cfg.write(f"\nbackend {backend_name}\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:
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":
@@ -160,15 +298,52 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
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")
# ===== 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 ssl_redirect_backend_name
# Check if HTTP redirect frontend exists
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
if not existing_http_frontend:
# Utwórz nowy HTTP redirect frontend (generic name)
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:
# Dodaj ACL do istniejącego HTTP frontendu
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(','))
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'],
})
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.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