Merge pull request 'new_functions_and_fixes' (#1) from new_functions_and_fixes into master
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
46
app.py
46
app.py
@@ -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')
|
||||
|
||||
172
log_parser.py
172
log_parser.py
@@ -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)
|
||||
|
||||
try:
|
||||
with open(log_file_path, 'r', encoding='utf-8', errors='ignore') as log_file:
|
||||
log_lines = log_file.readlines()
|
||||
|
||||
with open(log_file_path, 'r') 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,
|
||||
})
|
||||
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 []
|
||||
|
||||
parsed_entries.append({
|
||||
'timestamp': timestamp,
|
||||
'ip_address': ip_address,
|
||||
'http_method': http_method,
|
||||
'requested_url': requested_url,
|
||||
'xss_alert': xss_alert,
|
||||
'sql_alert': sql_alert,
|
||||
'put_method': put_method,
|
||||
'illegal_resource': illegal_resource,
|
||||
'webshell_alert': webshell_alert
|
||||
})
|
||||
return parsed_entries
|
||||
@@ -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"
|
||||
if not check_output:
|
||||
check_output = "Configuration file is valid"
|
||||
check_level = "success"
|
||||
|
||||
return result.returncode, out, level
|
||||
|
||||
check_output = ""
|
||||
check_level = "success"
|
||||
|
||||
if 'save_check' in request.form:
|
||||
_, check_output, check_level = run_check()
|
||||
|
||||
elif 'save_reload' in request.form:
|
||||
rc, out, level = run_check()
|
||||
check_output, check_level = out, level
|
||||
|
||||
if rc == 0:
|
||||
try:
|
||||
supervisor_result = subprocess.run(
|
||||
['pkill', '-f', 'haproxy'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if supervisor_result.returncode == 0:
|
||||
check_output += f"\n\nHAProxy Restarted:\n{supervisor_result.stdout}"
|
||||
else:
|
||||
check_output += (
|
||||
f"\n\nRestart attempt returned {supervisor_result.returncode}:\n"
|
||||
f"{supervisor_result.stdout}"
|
||||
)
|
||||
except Exception as e:
|
||||
check_output += f"\n\nRestart failed: {e}"
|
||||
if "Warning" in check_output or "Warnings" in check_output:
|
||||
check_level = "warning"
|
||||
check_output = f"⚠ {check_output}"
|
||||
else:
|
||||
check_output = f"✓ {check_output}"
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
restart_result = subprocess.run(
|
||||
['pkill', '-f', 'haproxy'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
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\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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
4
spawn.sh
4
spawn.sh
@@ -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
48
static/css/edit.css
Normal 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
133
static/js/editor.js
Normal 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
137
static/js/form.js
Normal 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();
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -1,105 +1,82 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
const $ = (sel, root=document) => root.querySelector(sel);
|
||||
const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
|
||||
// ===== HELPER FUNCTIONS =====
|
||||
const $ = (sel, root = document) => root.querySelector(sel);
|
||||
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||
const toggle = (on, el) => el && el.classList.toggle('d-none', !on);
|
||||
|
||||
// SSL fields
|
||||
const sslCheckbox = $('#ssl_checkbox');
|
||||
const sslFields = $('#ssl_fields');
|
||||
// ===== SSL FIELDS =====
|
||||
const sslCheckbox = $('#ssl_checkbox');
|
||||
const sslFields = $('#ssl_fields');
|
||||
sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields));
|
||||
|
||||
const toggle = (on, el) => el.classList.toggle('d-none', !on);
|
||||
// ===== DOS PROTECTION =====
|
||||
const dosCheckbox = $('#add_dos');
|
||||
const dosFields = $('#dos_fields');
|
||||
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
|
||||
|
||||
sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields));
|
||||
// ===== PROTOCOL CHANGE (HTTP/TCP) =====
|
||||
const protocolSelect = $('#protocol');
|
||||
const httpGroups = $$('.http-only, #forbidden_acl_container');
|
||||
const httpToggles = [
|
||||
$('#sql_injection_check'),
|
||||
$('#xss_check'),
|
||||
$('#remote_uploads_check'),
|
||||
$('#webshells_check'),
|
||||
$('#forward_for_check'),
|
||||
$('#add_acl_path'),
|
||||
$('#add_path_based'),
|
||||
$('#add_custom_acl'),
|
||||
];
|
||||
|
||||
// DOS
|
||||
const dosCheckbox = $('#add_dos');
|
||||
const dosFields = $('#dos_fields');
|
||||
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
|
||||
const forbiddenFields = $('#forbidden_fields');
|
||||
const pathFields = $('#base_redirect_fields');
|
||||
|
||||
// HTTP only groups
|
||||
const protocolSelect = $('#protocol');
|
||||
const httpGroups = $$('.http-only, #forbidden_acl_container');
|
||||
const httpToggles = [
|
||||
$('#sql_injection_check'),
|
||||
$('#xss_check'),
|
||||
$('#remote_uploads_check'),
|
||||
$('#webshells_check'),
|
||||
$('#forward_for_check'),
|
||||
$('#add_acl_path'),
|
||||
$('#add_path_based'),
|
||||
];
|
||||
const forbiddenFields = $('#forbidden_fields');
|
||||
const pathFields = $('#base_redirect_fields');
|
||||
const onProtocolChange = () => {
|
||||
const isHttp = protocolSelect?.value === 'http';
|
||||
httpGroups.forEach(el => toggle(isHttp, el));
|
||||
|
||||
const onProtocolChange = () => {
|
||||
const isHttp = protocolSelect?.value === 'http';
|
||||
httpGroups.forEach(el => toggle(isHttp, el));
|
||||
if (!isHttp) {
|
||||
// hide optional groups if protocol != http
|
||||
[forbiddenFields, pathFields].forEach(el => el && toggle(false, el));
|
||||
httpToggles.forEach(input => { if (input) input.checked = false; });
|
||||
}
|
||||
};
|
||||
protocolSelect?.addEventListener('change', onProtocolChange);
|
||||
onProtocolChange();
|
||||
if (!isHttp) {
|
||||
[forbiddenFields, pathFields].forEach(el => toggle(false, el));
|
||||
httpToggles.forEach(input => {
|
||||
if (input) input.checked = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ACL
|
||||
const aclCheckbox = $('#add_acl');
|
||||
const aclFields = $('#acl_fields');
|
||||
aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields));
|
||||
protocolSelect?.addEventListener('change', onProtocolChange);
|
||||
onProtocolChange();
|
||||
|
||||
// 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 SSL REDIRECT =====
|
||||
const backendSslCheckbox = $('#backend_ssl_redirect');
|
||||
const backendSslFields = $('#backend_ssl_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();
|
||||
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -1,34 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "" %}
|
||||
{% block title %}HAProxy • Edit{% endblock %}
|
||||
{% block breadcrumb %}<nav aria-label="breadcrumb" class="mb-3"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item active" aria-current="page">Edytor</li></ol></nav>{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-3 text-muted">Edit HAProxy configuration</h4>
|
||||
<form method="POST" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="haproxy_config" class="form-label">Config</label>
|
||||
<textarea class="form-control" name="haproxy_config" id="haproxy_config" rows="20">{{ config_content }}</textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-warning" id="save_check" name="save_check">
|
||||
<i class="bi bi-search me-1"></i> Check & Save
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="save_reload">
|
||||
<i class="bi bi-arrow-repeat me-1"></i> Check & Restart
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if check_output %}
|
||||
<div class="alert alert-{{ check_level|default('success') }}" role="alert">
|
||||
<pre class="mb-0">{{ check_output }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% set active_page = "edit" %}
|
||||
|
||||
{% block title %}HAProxy • Configuration Editor{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Edit Configuration</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- CodeMirror CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/material-darker.min.css">
|
||||
|
||||
{% if check_output %}
|
||||
<div class="alert alert-{{ check_level|default('info') }} alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-{% if check_level == 'success' %}check-circle{% elif check_level == 'danger' %}exclamation-circle{% elif check_level == 'warning' %}exclamation-triangle{% else %}info-circle{% endif %} me-2"></i>
|
||||
<strong>
|
||||
{% if check_level == 'success' %}Configuration Saved
|
||||
{% elif check_level == 'danger' %}Configuration Error
|
||||
{% elif check_level == 'warning' %}Warning
|
||||
{% else %}Information{% endif %}
|
||||
</strong>
|
||||
<div class="mt-2 small" style="font-family: monospace; background-color: rgba(0,0,0,0.1); padding: 10px; border-radius: 4px; max-height: 250px; overflow-y: auto; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word;">{{ check_output }}</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block page_js %}
|
||||
<script src="{{ url_for('static', filename='js/edit.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Editor Section -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0"><i class="bi bi-pencil-square me-2"></i>HAProxy Configuration Editor</h5>
|
||||
</div>
|
||||
<small class="text-white-50">Real-time editor with syntax highlighting</small>
|
||||
</div>
|
||||
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<form method="post" id="edit_form">
|
||||
<!-- Editor Container -->
|
||||
<div style="border-bottom: 1px solid #dee2e6;">
|
||||
<textarea id="haproxy_editor" name="haproxy_config" style="display: none;">{{ config_content }}</textarea>
|
||||
<!-- Fallback textarea (hidden by default) -->
|
||||
<textarea id="haproxy_config" name="haproxy_config" style="display: none; width: 100%; border: none; font-family: monospace; font-size: 13px; resize: none; padding: 12px; min-height: 500px; background: #1e1e1e; color: #e8e8e8;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="p-3 bg-dark d-flex justify-content-between align-items-center flex-wrap gap-2" style="border-top: 1px solid #444;">
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success btn-sm" name="action" value="check">
|
||||
<i class="bi bi-check-circle me-1"></i>Validate Configuration
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm" name="action" value="save">
|
||||
<i class="bi bi-save me-1"></i>Save & Restart HAProxy
|
||||
</button>
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted" style="color: #aaa !important;">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<span id="line_col">Line 1, Col 1</span> |
|
||||
<span id="char_count">0</span> characters
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CodeMirror JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/nginx/nginx.min.js"></script>
|
||||
|
||||
<!-- Editor JS -->
|
||||
<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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="card-body">
|
||||
<form method="post" action="/" id="fe-be-form" novalidate>
|
||||
<h5 class="mb-3"><i class="fas fa-globe me-2"></i>New frontend</h5>
|
||||
{% if message %}
|
||||
<div class="alert {% if 'already exists' in message %}alert-danger{% else %}alert-success{% endif %} alert-dismissible" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="frontend_name">Name</label>
|
||||
<input type="text" class="form-control" name="frontend_name" id="frontend_name" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="frontend_ip">IP</label>
|
||||
<input type="text" class="form-control" name="frontend_ip" id="frontend_ip" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="frontend_port">Port</label>
|
||||
<input type="number" class="form-control" name="frontend_port" id="frontend_port" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input type="checkbox" class="form-check-input" id="ssl_checkbox" name="ssl_checkbox">
|
||||
<label class="form-check-label" for="ssl_checkbox"><i class="fas fa-lock me-2"></i>SSL cert</label>
|
||||
</div>
|
||||
<div class="row g-3 mt-1 d-none" id="ssl_fields">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="ssl_cert_path">Certificate path (put in /ssl/)</label>
|
||||
<input type="text" id="ssl_cert_path" class="form-control" name="ssl_cert_path">
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="ssl_redirect_checkbox" name="ssl_redirect_checkbox">
|
||||
<label class="form-check-label" for="ssl_redirect_checkbox"><i class="fas fa-arrow-circle-right me-2"></i>Redirect do HTTPS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="lb_method">Metoda LB</label>
|
||||
<select class="form-select" name="lb_method" id="lb_method">
|
||||
<option value="roundrobin">Round Robin</option>
|
||||
<option value="leastconn">Least Connections</option>
|
||||
<option value="source">Source</option>
|
||||
<option value="wrr">WRR</option>
|
||||
<option value="wlc">WLC</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="protocol">Tryb</label>
|
||||
<select class="form-select" name="protocol" id="protocol" required>
|
||||
<option value="" disabled selected>--Select--</option>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="http">HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input type="checkbox" class="form-check-input" name="add_dos" id="add_dos">
|
||||
<label class="form-check-label" for="add_dos"><i class="fas fa-shield-alt me-2"></i>DOS protection</label>
|
||||
</div>
|
||||
<div class="row g-3 mt-1 d-none" id="dos_fields">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="limit_requests">Limit (np. 20)</label>
|
||||
<input type="text" class="form-control" name="limit_requests" id="limit_requests">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="ban_duration">Ban (np. 15s)</label>
|
||||
<input type="text" class="form-control" name="ban_duration" id="ban_duration">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2 d-none http-only" id="http_extras">
|
||||
<div class="col-12">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="sql_injection_check" name="sql_injection_check">
|
||||
<label class="form-check-label" for="sql_injection_check"><i class="fas fa-shield-alt me-2"></i>SQLi</label>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary">{{ frontend_count|default(0) }}</h5>
|
||||
<p class="card-text"><i class="bi bi-diagram-2"></i> Frontends</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="xss_check" name="xss_check">
|
||||
<label class="form-check-label" for="xss_check"><i class="fas fa-shield-alt me-2"></i>XSS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success">{{ backend_count|default(0) }}</h5>
|
||||
<p class="card-text"><i class="bi bi-hdd-rack"></i> Backends</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remote_uploads_check" name="remote_uploads_check">
|
||||
<label class="form-check-label" for="remote_uploads_check"><i class="fas fa-shield-alt me-2"></i>Remote uploads</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning">{{ acl_count|default(0) }}</h5>
|
||||
<p class="card-text"><i class="bi bi-shield"></i> ACLs</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="webshells_check" name="webshells_check">
|
||||
<label class="form-check-label" for="webshells_check"><i class="fas fa-shield-alt me-2"></i>Webshells</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info">L7: {{ layer7_count|default(0) }} / L4: {{ layer4_count|default(0) }}</h5>
|
||||
<p class="card-text"><i class="bi bi-layers"></i> Layers</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="forward_for_check" name="forward_for_check">
|
||||
<label class="form-check-label" for="forward_for_check"><i class="fas fa-network-wired me-2"></i>forwardfor</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input type="checkbox" class="form-check-input" name="add_acl" id="add_acl">
|
||||
<label class="form-check-label" for="add_acl"><i class="fas fa-user-lock me-2"></i>ACL for frontend</label>
|
||||
</div>
|
||||
<div class="row g-3 mt-1 d-none" id="acl_fields">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="acl">ACL</label>
|
||||
<input type="text" class="form-control" name="acl" id="acl" placeholder="acl_name">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="acl_action">Action</label>
|
||||
<input type="text" class="form-control" name="acl_action" id="acl_action" placeholder="hdr(host) -i test.com">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="backend_name_acl">Backend</label>
|
||||
<input type="text" class="form-control" name="backend_name_acl" id="backend_name_acl" placeholder="somebackend">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3 http-only d-none" id="forbidden_acl_container">
|
||||
<input type="checkbox" class="form-check-input" name="add_acl_path" id="add_acl_path">
|
||||
<label class="form-check-label" for="add_acl_path"><i class="fas fa-ban me-2"></i>Block path</label>
|
||||
</div>
|
||||
<div class="row g-3 mt-1 d-none" id="forbidden_fields">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="forbidden_name">ACL name</label>
|
||||
<input type="text" class="form-control" name="forbidden_name" id="forbidden_name">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="allowed_ip">Allowed IP</label>
|
||||
<input type="text" class="form-control" name="allowed_ip" id="allowed_ip">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="forbidden_path">Path (ex. /admin)</label>
|
||||
<input type="text" class="form-control" name="forbidden_path" id="forbidden_path">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3 http-only d-none" id="path_based_container">
|
||||
<input type="checkbox" class="form-check-input" name="add_path_based" id="add_path_based">
|
||||
<label class="form-check-label" for="add_path_based"><i class="fas fa-arrow-circle-right me-2"></i>Path-based redirect</label>
|
||||
</div>
|
||||
<div class="row g-3 mt-1 d-none" id="base_redirect_fields">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="redirect_domain_name">Domena docelowa</label>
|
||||
<input type="text" class="form-control" name="redirect_domain_name" id="redirect_domain_name" placeholder="test2.com:8888">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="root_redirect">Root path</label>
|
||||
<input type="text" class="form-control" name="root_redirect" id="root_redirect" placeholder="/">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="redirect_to">Redirect to</label>
|
||||
<input type="text" class="form-control" name="redirect_to" id="redirect_to" placeholder="/test">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5 class="mb-3"><i class="fas fa-sitemap me-2"></i>Backend pool</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="backend_name">Backend name</label>
|
||||
<input type="text" class="form-control" name="backend_name" id="backend_name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="backend_servers_container" class="mt-3">
|
||||
<div class="row g-3 backend-server-row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="name1">Server name</label>
|
||||
<input type="text" id="name1" class="form-control" name="backend_server_names[]" placeholder="server1" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="ip1">IP</label>
|
||||
<input type="text" id="ip1" class="form-control" name="backend_server_ips[]" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="port1">Port</label>
|
||||
<input type="number" id="port1" class="form-control" name="backend_server_ports[]" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="maxconn1">MaxConn</label>
|
||||
<input type="number" id="maxconn1" class="form-control" name="backend_server_maxconns[]">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="button" class="btn btn-secondary" id="add_backend_btn"><i class="bi bi-plus-lg me-1"></i>Add backend</button>
|
||||
<button type="submit" class="btn btn-success" id="success_btn"><i class="bi bi-check2-circle me-1"></i>Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block page_js %}
|
||||
|
||||
{% if message %}
|
||||
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-{% if message_type == 'success' %}check-circle{% elif message_type == 'danger' %}exclamation-circle{% else %}info-circle{% endif %} me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="needs-validation">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add New Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- FRONTEND SECTION -->
|
||||
<h6 class="text-primary mb-3"><i class="bi bi-hdd-network me-2"></i>Frontend Configuration</h6>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="frontend_ip" class="form-label">Listener IP</label>
|
||||
<input type="text" class="form-control" id="frontend_ip" name="frontend_ip"
|
||||
placeholder="0.0.0.0" value="0.0.0.0" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="frontend_port" class="form-label">Listener Port</label>
|
||||
<input type="number" class="form-control" id="frontend_port" name="frontend_port"
|
||||
placeholder="443" value="443" min="1" max="65535" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="frontend_hostname" class="form-label">Frontend Hostname</label>
|
||||
<input type="text" class="form-control" id="frontend_hostname" name="frontend_hostname"
|
||||
placeholder="e.g. host.domain.com" required>
|
||||
<small class="text-muted d-block mt-1">Frontend name will be generated automatically</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="protocol" class="form-label">Protocol</label>
|
||||
<select class="form-select" id="protocol" name="protocol" required>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="tcp">TCP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="lb_method" class="form-label">Load Balancing Method</label>
|
||||
<select class="form-select" id="lb_method" name="lb_method" required>
|
||||
<option value="roundrobin">Round Robin</option>
|
||||
<option value="leastconn">Least Connections</option>
|
||||
<option value="source">Source IP Hash</option>
|
||||
<option value="uri">URI Hash</option>
|
||||
<option value="static-rr">Static Round Robin (WRR)</option>
|
||||
<option value="no-lb">No Load Balancing (single host)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSL Section -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="ssl_checkbox" name="ssl_checkbox">
|
||||
<label class="form-check-label" for="ssl_checkbox">
|
||||
<i class="bi bi-lock me-1"></i>Use SSL (HTTPS)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3 d-none" id="ssl_fields">
|
||||
<div class="col-md-12">
|
||||
<label for="ssl_cert_path" class="form-label">SSL Certificate Path</label>
|
||||
<input type="text" class="form-control" id="ssl_cert_path" name="ssl_cert_path"
|
||||
value="/app/ssl/haproxy-configurator.pem">
|
||||
<small class="text-muted">Full path to .pem file</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="ssl_redirect_checkbox"
|
||||
name="ssl_redirect_checkbox">
|
||||
<label class="form-check-label" for="ssl_redirect_checkbox">
|
||||
Redirect HTTP to HTTPS
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
||||
<!-- BACKEND SECTION -->
|
||||
<h6 class="text-primary mb-3"><i class="bi bi-hdd-rack me-2"></i>Backend Configuration</h6>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-12">
|
||||
<label for="backend_name" class="form-label">Backend Name</label>
|
||||
<input type="text" class="form-control" id="backend_name" name="backend_name"
|
||||
placeholder="e.g. be_web" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backend servers -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Backend Servers</label>
|
||||
<div id="backend_servers_container">
|
||||
<div class="row g-3 backend-server-row">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control" name="backend_server_names[]"
|
||||
placeholder="server1" value="server1" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control" name="backend_server_ips[]"
|
||||
placeholder="192.168.1.10" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number" class="form-control" name="backend_server_ports[]"
|
||||
placeholder="80" min="1" max="65535" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number" class="form-control" name="backend_server_maxconns[]"
|
||||
placeholder="100">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm w-100 remove-server" style="visibility: hidden;">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm mt-2" id="add_backend_btn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Server
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Health Check -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="health_check" name="health_check">
|
||||
<label class="form-check-label" for="health_check">
|
||||
<i class="bi bi-heart-pulse me-1"></i>Enable Health Check
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 d-none" id="health_check_fields">
|
||||
<label for="health_check_link" class="form-label">Health Check Path</label>
|
||||
<input type="text" class="form-control" id="health_check_link" name="health_check_link"
|
||||
value="/" placeholder="/">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3" style="display: none;" id="tcp_health_check">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="health_check2" name="health_check2">
|
||||
<label class="form-check-label" for="health_check2">
|
||||
<i class="bi bi-heart-pulse me-1"></i>Enable TCP Health Check
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Session -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="sticky_session" name="sticky_session">
|
||||
<label class="form-check-label" for="sticky_session">
|
||||
<i class="bi bi-pin-angle me-1"></i>Sticky Session
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 d-none" id="sticky_fields">
|
||||
<select class="form-select" id="sticky_session_type" name="sticky_session_type">
|
||||
<option value="cookie">Cookie-based</option>
|
||||
<option value="source">Source IP-based</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- HEADERS & SECURITY SECTION -->
|
||||
<h6 class="text-primary mb-3"><i class="bi bi-shield-lock me-2"></i>Headers & Security</h6>
|
||||
|
||||
<!-- Custom Headers -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="add_header" name="add_header">
|
||||
<label class="form-check-label" for="add_header">
|
||||
<i class="bi bi-tag me-1"></i>Add Custom Header
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-none" id="header_fields">
|
||||
<input type="text" class="form-control" id="header_name" name="header_name"
|
||||
placeholder="e.g. X-Custom-Header">
|
||||
</div>
|
||||
<div class="col-md-6 d-none" id="header_fields">
|
||||
<input type="text" class="form-control" id="header_value" name="header_value"
|
||||
placeholder="e.g. custom-value">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Header Removal -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="del_server_header"
|
||||
name="del_server_header">
|
||||
<label class="form-check-label" for="del_server_header">
|
||||
<i class="bi bi-shield-lock me-1"></i>Hide Server Header
|
||||
</label>
|
||||
<small class="text-muted d-block">Adds: <code>http-response del-header Server</code></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Forward For -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="forward_for_check"
|
||||
name="forward_for_check" checked>
|
||||
<label class="form-check-label" for="forward_for_check">
|
||||
<i class="bi bi-arrow-right me-1"></i>Forward For (X-Forwarded-For)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- PROTECTION SECTION -->
|
||||
<h6 class="text-primary mb-3"><i class="bi bi-bug me-2"></i>Protection</h6>
|
||||
|
||||
<!-- DOS Protection -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="add_dos" name="add_dos">
|
||||
<label class="form-check-label" for="add_dos">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>DOS/DDoS Protection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-none" id="dos_fields">
|
||||
<label for="ban_duration" class="form-label">Ban Duration</label>
|
||||
<input type="text" class="form-control" id="ban_duration" name="ban_duration"
|
||||
value="30m" placeholder="30m">
|
||||
<small class="text-muted">e.g. 30m, 1h, 24h</small>
|
||||
</div>
|
||||
<div class="col-md-6 d-none" id="dos_fields">
|
||||
<label for="limit_requests" class="form-label">Request Limit (per min)</label>
|
||||
<input type="number" class="form-control" id="limit_requests" name="limit_requests"
|
||||
value="100" min="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SQL Injection -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="sql_injection_check"
|
||||
name="sql_injection_check">
|
||||
<label class="form-check-label" for="sql_injection_check">
|
||||
<i class="bi bi-database-exclamation me-1"></i>SQL Injection Protection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- XSS -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="xss_check" name="xss_check">
|
||||
<label class="form-check-label" for="xss_check">
|
||||
<i class="bi bi-code-slash me-1"></i>XSS Protection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remote Uploads -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="remote_uploads_check"
|
||||
name="remote_uploads_check">
|
||||
<label class="form-check-label" for="remote_uploads_check">
|
||||
<i class="bi bi-cloud-upload me-1"></i>Block Remote Uploads
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Webshells -->
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="webshells_check"
|
||||
name="webshells_check">
|
||||
<label class="form-check-label" for="webshells_check">
|
||||
<i class="bi bi-shield-exclamation me-1"></i>Block Webshells
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- CUSTOM ACL SECTION -->
|
||||
<h6 class="text-primary mb-3"><i class="bi bi-shuffle me-2"></i>Custom ACL Rules (Advanced)</h6>
|
||||
|
||||
<div class="row g-3 mb-3 http-only">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="add_custom_acl" name="add_custom_acl">
|
||||
<label class="form-check-label" for="add_custom_acl">
|
||||
<i class="bi bi-sliders me-1"></i>Add Custom ACL Rule
|
||||
</label>
|
||||
<small class="text-muted d-block">Create additional routing or blocking rules</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom ACL Fields -->
|
||||
<div class="row g-3 mb-3 http-only d-none" id="custom_acl_fields">
|
||||
<div class="col-md-3">
|
||||
<label for="custom_acl_name" class="form-label">ACL Name</label>
|
||||
<input type="text" class="form-control" id="custom_acl_name" name="custom_acl_name"
|
||||
placeholder="e.g. is_admin_path">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="custom_acl_type" class="form-label">Rule Type</label>
|
||||
<select class="form-select" id="custom_acl_type" name="custom_acl_type">
|
||||
<option value="path_beg">Path Begins With</option>
|
||||
<option value="path_end">Path Ends With</option>
|
||||
<option value="path_sub">Path Contains</option>
|
||||
<option value="hdr">Header Contains</option>
|
||||
<option value="src">Source IP</option>
|
||||
<option value="method">HTTP Method</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="custom_acl_value" class="form-label">Rule Value</label>
|
||||
<input type="text" class="form-control" id="custom_acl_value" name="custom_acl_value"
|
||||
placeholder="e.g. /admin, api, 192.168.1.0/24">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="custom_acl_action" class="form-label">Action</label>
|
||||
<select class="form-select" id="custom_acl_action" name="custom_acl_action">
|
||||
<option value="route">Route to Backend</option>
|
||||
<option value="deny">Block (Deny)</option>
|
||||
<option value="redirect">Redirect</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 d-none" id="acl_backend_select">
|
||||
<label for="custom_acl_backend" class="form-label">Target Backend</label>
|
||||
<input type="text" class="form-control" id="custom_acl_backend" name="custom_acl_backend"
|
||||
placeholder="e.g. be_admin">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 d-none" id="acl_redirect_select">
|
||||
<label for="custom_acl_redirect_url" class="form-label">Redirect URL</label>
|
||||
<input type="text" class="form-control" id="custom_acl_redirect_url" name="custom_acl_redirect_url"
|
||||
placeholder="e.g. https://example.com/new-path">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- SUBMIT BUTTON -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-check-circle me-2"></i>Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/form.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 content %}
|
||||
<h3 class="mb-4" id="status_header">Status 403 Forbidden</h3>
|
||||
{% if entries %}
|
||||
<div class="vstack gap-3">
|
||||
{% for entry in entries %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div><strong>Czas:</strong> {{ entry['timestamp'] }}</div>
|
||||
<div><strong>IP:</strong> {{ entry['ip_address'] }}</div>
|
||||
<div><strong>Metoda:</strong> {{ entry['http_method'] }}</div>
|
||||
<div><strong>URL:</strong> {{ entry['requested_url'] }}</div>
|
||||
<div><strong>Status:</strong> <span class="badge bg-danger">403</span></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if entry['xss_alert'] %}
|
||||
<p class="mb-1"><button class="btn btn-sm btn-outline-danger" data-bs-toggle="collapse" data-bs-target="#xssCollapse{{ loop.index }}"><i class="bi bi-bug"></i> XSS alert</button></p>
|
||||
<div id="xssCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['xss_alert'] }}</pre></div>
|
||||
{% endif %}
|
||||
{% if entry['sql_alert'] %}
|
||||
<p class="mb-1"><button class="btn btn-sm btn-outline-warning" data-bs-toggle="collapse" data-bs-target="#sqlCollapse{{ loop.index }}"><i class="bi bi-database-exclamation"></i> SQLi alert</button></p>
|
||||
<div id="sqlCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-warning">{{ entry['sql_alert'] }}</pre></div>
|
||||
{% endif %}
|
||||
{% if entry['put_method'] %}
|
||||
<p class="mb-1"><button class="btn btn-sm btn-outline-info" data-bs-toggle="collapse" data-bs-target="#putCollapse{{ loop.index }}"><i class="bi bi-upload"></i> PUT alert</button></p>
|
||||
<div id="putCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-info">{{ entry['put_method'] }}</pre></div>
|
||||
{% endif %}
|
||||
{% if entry['illegal_resource'] %}
|
||||
<p class="mb-1"><button class="btn btn-sm btn-outline-light" data-bs-toggle="collapse" data-bs-target="#illegalCollapse{{ loop.index }}"><i class="bi bi-shield-x"></i> Nielegalny zasób</button></p>
|
||||
<div id="illegalCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-light">{{ entry['illegal_resource'] }}</pre></div>
|
||||
{% endif %}
|
||||
{% if entry['webshell_alert'] %}
|
||||
<p class="mb-1"><button class="btn btn-sm btn-outline-danger" data-bs-toggle="collapse" data-bs-target="#webshellCollapse{{ loop.index }}"><i class="bi bi-file-earmark-code"></i> WebShell alert</button></p>
|
||||
<div id="webshellCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['webshell_alert'] }}</pre></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info"><i class="bi bi-info-circle me-1"></i>No data.</div>
|
||||
{% endif %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Access Logs</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-file-text me-2"></i>HAProxy Access Logs</h5>
|
||||
</div>
|
||||
<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'] %}
|
||||
<span class="badge bg-danger">XSS</span>
|
||||
{% endif %}
|
||||
{% if entry['sql_alert'] %}
|
||||
<span class="badge bg-danger">SQL</span>
|
||||
{% endif %}
|
||||
{% if entry['put_method'] %}
|
||||
<span class="badge bg-warning">PUT</span>
|
||||
{% endif %}
|
||||
{% if entry['webshell_alert'] %}
|
||||
<span class="badge bg-danger">Webshell</span>
|
||||
{% endif %}
|
||||
{% 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-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 %}
|
||||
|
||||
@@ -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:
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[HAPROXY_CONFIG] Error checking frontend: {e}", flush=True)
|
||||
with open(HAPROXY_CFG, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
return False
|
||||
# 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 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
|
||||
|
||||
@@ -79,42 +131,117 @@ def count_frontends_and_backends():
|
||||
return frontend_count, backend_count, acl_count, layer7_count, layer4_count
|
||||
|
||||
def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name,
|
||||
backend_servers, health_check, health_check_tcp, health_check_link, sticky_session,
|
||||
add_header, header_name, header_value, sticky_session_type, is_acl, acl_name,
|
||||
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
|
||||
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
|
||||
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
|
||||
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells):
|
||||
backend_servers, health_check, health_check_tcp, health_check_link, sticky_session,
|
||||
add_header, header_name, header_value, sticky_session_type, is_acl, acl_name,
|
||||
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
|
||||
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
|
||||
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
|
||||
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells,
|
||||
del_server_header=False, backend_ssl_redirect=False, ssl_redirect_backend_name='',
|
||||
ssl_redirect_port='80', frontend_hostname='', add_custom_acl=False,
|
||||
custom_acl_name='', custom_acl_type='path_beg', custom_acl_value='',
|
||||
custom_acl_action='route', custom_acl_backend='', custom_acl_redirect_url=''):
|
||||
|
||||
# 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:
|
||||
# ===== BACKEND =====
|
||||
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
|
||||
|
||||
if not is_no_lb:
|
||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||
|
||||
if sticky_session and not is_no_lb:
|
||||
if sticky_session_type == "cookie":
|
||||
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
|
||||
elif sticky_session_type == "source":
|
||||
haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n")
|
||||
haproxy_cfg.write(f" stick on src\n")
|
||||
|
||||
if health_check and protocol == 'http':
|
||||
haproxy_cfg.write(f" option httpchk GET {health_check_link}\n")
|
||||
elif health_check_tcp and protocol == 'tcp':
|
||||
haproxy_cfg.write(f" option tcp-check\n")
|
||||
|
||||
if add_header:
|
||||
haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n")
|
||||
|
||||
if del_server_header:
|
||||
haproxy_cfg.write(f" http-response del-header Server\n")
|
||||
|
||||
if forward_for:
|
||||
haproxy_cfg.write(f" option forwardfor\n")
|
||||
|
||||
# Add servers
|
||||
for server_name, server_ip, server_port, maxconn in backend_servers:
|
||||
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
|
||||
|
||||
if health_check and protocol == 'http':
|
||||
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n")
|
||||
else:
|
||||
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
|
||||
|
||||
# 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:
|
||||
haproxy_cfg.write(f"\nfrontend {frontend_name}\n")
|
||||
|
||||
if is_frontend_exist(frontend_name, frontend_ip, frontend_port):
|
||||
return "Frontend or Port already exists. Cannot add duplicate."
|
||||
|
||||
# ===== PRIMARY FRONTEND (GENERIC NAME) =====
|
||||
haproxy_cfg.write(f"\nfrontend {generic_frontend_name}\n")
|
||||
haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}")
|
||||
|
||||
if use_ssl:
|
||||
haproxy_cfg.write(f" ssl crt {ssl_cert_path}")
|
||||
|
||||
haproxy_cfg.write("\n")
|
||||
|
||||
if https_redirect:
|
||||
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
|
||||
|
||||
if forward_for:
|
||||
haproxy_cfg.write(f" option forwardfor\n")
|
||||
# Headers zaraz po BIND/CERT
|
||||
haproxy_cfg.write(f" http-request set-header X-Forwarded-For %[src]\n")
|
||||
if use_ssl:
|
||||
haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto https\n")
|
||||
else:
|
||||
haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto http\n")
|
||||
|
||||
haproxy_cfg.write(f" mode {protocol}\n")
|
||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||
|
||||
# Add protection rules
|
||||
# ACL dla pierwszego vhost
|
||||
acl_name_sanitized = None
|
||||
if frontend_hostname:
|
||||
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}"
|
||||
haproxy_cfg.write(f" acl {acl_name_sanitized} hdr(host) -i {frontend_hostname}\n")
|
||||
|
||||
if not is_no_lb:
|
||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||
if forward_for:
|
||||
haproxy_cfg.write(f" option forwardfor\n")
|
||||
|
||||
# Protections
|
||||
if is_dos:
|
||||
haproxy_cfg.write(f" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n")
|
||||
haproxy_cfg.write(f" http-request track-sc0 src\n")
|
||||
@@ -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")
|
||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||
if del_server_header:
|
||||
haproxy_cfg.write(f" http-response del-header Server\n")
|
||||
|
||||
if sticky_session:
|
||||
# Backend routing
|
||||
if acl_name_sanitized:
|
||||
haproxy_cfg.write(f" use_backend {unique_backend_name} if {acl_name_sanitized}\n")
|
||||
else:
|
||||
haproxy_cfg.write(f" default_backend {unique_backend_name}\n")
|
||||
|
||||
# ===== BACKEND =====
|
||||
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
|
||||
|
||||
if not is_no_lb:
|
||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||
|
||||
if sticky_session and not is_no_lb:
|
||||
if sticky_session_type == "cookie":
|
||||
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
|
||||
elif sticky_session_type == "source":
|
||||
@@ -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")
|
||||
|
||||
return "Configuration updated successfully!"
|
||||
# ===== REDIRECT HTTP -> HTTPS (GENERIC NAME) =====
|
||||
if backend_ssl_redirect and ssl_redirect_backend_name:
|
||||
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else 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}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import requests
|
||||
import csv
|
||||
|
||||
HAPROXY_STATS_URL = 'http://127.0.0.1:8484/;csv'
|
||||
HAPROXY_STATS_URL = 'http://127.0.0.1:8404/stats;csv'
|
||||
|
||||
def fetch_haproxy_stats():
|
||||
try:
|
||||
@@ -13,18 +13,58 @@ def fetch_haproxy_stats():
|
||||
|
||||
def parse_haproxy_stats(stats_data):
|
||||
data = []
|
||||
header_row = stats_data.splitlines()[0].replace('# ', '')
|
||||
reader = csv.DictReader(stats_data.splitlines(), fieldnames=header_row.split(','))
|
||||
|
||||
lines = [line for line in stats_data.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
return data
|
||||
|
||||
header_row = lines[0].replace('# ', '')
|
||||
|
||||
# Parse CSV
|
||||
reader = csv.DictReader(lines, fieldnames=header_row.split(','))
|
||||
next(reader)
|
||||
|
||||
for row in reader:
|
||||
if row['svname'] != 'BACKEND':
|
||||
data.append({
|
||||
'frontend_name': row['pxname'],
|
||||
'server_name': row['svname'],
|
||||
'4xx_errors': row['hrsp_4xx'],
|
||||
'5xx_errors': row['hrsp_5xx'],
|
||||
'bytes_in_mb': f'{float(row["bin"]) / (1024 * 1024):.2f}',
|
||||
'bytes_out_mb': f'{float(row["bout"]) / (1024 * 1024):.2f}',
|
||||
'conn_tot': row['conn_tot'],
|
||||
})
|
||||
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
|
||||
Reference in New Issue
Block a user