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 os
|
||||||
import configparser
|
import sys
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
from flask import Flask, render_template, render_template_string
|
||||||
from routes.main_routes import main_bp
|
from routes.main_routes import main_bp
|
||||||
from routes.edit_routes import edit_bp
|
from routes.edit_routes import edit_bp
|
||||||
from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats
|
from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats
|
||||||
from auth.auth_middleware import setup_auth
|
from auth.auth_middleware import setup_auth
|
||||||
from log_parser import parse_log_file
|
from log_parser import parse_log_file
|
||||||
import os
|
from utils.haproxy_config import update_haproxy_config, count_frontends_and_backends
|
||||||
import sys
|
|
||||||
from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends
|
|
||||||
|
|
||||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
@@ -73,23 +74,23 @@ try:
|
|||||||
certificate_path = config2.get('ssl', 'certificate_path')
|
certificate_path = config2.get('ssl', 'certificate_path')
|
||||||
private_key_path = config2.get('ssl', 'private_key_path')
|
private_key_path = config2.get('ssl', 'private_key_path')
|
||||||
else:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
if not os.path.exists(certificate_path):
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
if not os.path.exists(private_key_path):
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
||||||
ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path)
|
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:
|
except Exception as e:
|
||||||
print(f"[APP] ✗ SSL error: {e}", flush=True)
|
print(f"[APP] SSL error: {e}", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,11 +100,28 @@ def display_haproxy_stats():
|
|||||||
parsed_stats = parse_haproxy_stats(haproxy_stats)
|
parsed_stats = parse_haproxy_stats(haproxy_stats)
|
||||||
return render_template('statistics.html', stats=parsed_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'
|
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')
|
@app.route('/home')
|
||||||
|
|||||||
150
log_parser.py
150
log_parser.py
@@ -1,7 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
def parse_log_file(log_file_path):
|
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 = []
|
parsed_entries = []
|
||||||
|
|
||||||
|
# Security threat patterns
|
||||||
xss_patterns = [
|
xss_patterns = [
|
||||||
r'<\s*script\s*',
|
r'<\s*script\s*',
|
||||||
r'javascript:',
|
r'javascript:',
|
||||||
@@ -12,92 +18,118 @@ def parse_log_file(log_file_path):
|
|||||||
r'<\s*input\s*[^>]*\s*value\s*=?',
|
r'<\s*input\s*[^>]*\s*value\s*=?',
|
||||||
r'<\s*form\s*action\s*=?',
|
r'<\s*form\s*action\s*=?',
|
||||||
r'<\s*svg\s*on\w+\s*=?',
|
r'<\s*svg\s*on\w+\s*=?',
|
||||||
r'script',
|
r'alert\s*\(',
|
||||||
r'alert',
|
|
||||||
r'onerror',
|
r'onerror',
|
||||||
r'onload',
|
r'onload',
|
||||||
r'javascript'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sql_patterns = [
|
sql_patterns = [
|
||||||
r';',
|
r'(union|select|insert|update|delete|drop)\s+(from|into|table)',
|
||||||
r'substring',
|
r';\s*(union|select|insert|update|delete|drop)',
|
||||||
r'extract',
|
r'substring\s*\(',
|
||||||
r'union\s+all',
|
r'extract\s*\(',
|
||||||
r'order\s+by',
|
r'order\s+by\s+\d+',
|
||||||
r'--\+',
|
r'--\+',
|
||||||
r'union',
|
r'1\s*=\s*1',
|
||||||
r'select',
|
r'@@\w+',
|
||||||
r'insert',
|
|
||||||
r'update',
|
|
||||||
r'delete',
|
|
||||||
r'drop',
|
|
||||||
r'@@',
|
|
||||||
r'1=1',
|
|
||||||
r'`1',
|
r'`1',
|
||||||
r'union',
|
|
||||||
r'select',
|
|
||||||
r'insert',
|
|
||||||
r'update',
|
|
||||||
r'delete',
|
|
||||||
r'drop',
|
|
||||||
r'@@',
|
|
||||||
r'1=1',
|
|
||||||
r'`1'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
webshells_patterns = [
|
webshells_patterns = [
|
||||||
r'payload',
|
r'eval\s*\(',
|
||||||
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'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)
|
# Compile patterns
|
||||||
combined_sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE)
|
xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE)
|
||||||
combined_webshells_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE)
|
sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE)
|
||||||
|
webshell_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE)
|
||||||
|
|
||||||
with open(log_file_path, 'r') as log_file:
|
try:
|
||||||
|
with open(log_file_path, 'r', encoding='utf-8', errors='ignore') as log_file:
|
||||||
log_lines = log_file.readlines()
|
log_lines = log_file.readlines()
|
||||||
|
|
||||||
for line in log_lines:
|
for line in log_lines:
|
||||||
if " 403 " in line: # Check if the line contains " 403 " indicating a 403 status code
|
if not line.strip():
|
||||||
match = re.search(r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"\s*(GET|POST|PUT|DELETE)\s+([^"]+)"', line)
|
continue
|
||||||
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 combined_xss_pattern.search(line):
|
try:
|
||||||
xss_alert = 'Possible XSS Attack Was Identified.'
|
# Extract syslog header
|
||||||
else:
|
syslog_match = re.search(
|
||||||
xss_alert = ''
|
r'<\d+>(\w+\s+\d+\s+\d+:\d+:\d+).*haproxy\[\d+\]:\s+',
|
||||||
if combined_sql_pattern.search(line):
|
line
|
||||||
sql_alert = 'Possible SQL Injection Attempt Was Made.'
|
)
|
||||||
else:
|
|
||||||
sql_alert = ''
|
|
||||||
if "PUT" in line:
|
|
||||||
put_method = 'Possible Remote File Upload Attempt Was Made.'
|
|
||||||
else:
|
|
||||||
put_method = ''
|
|
||||||
|
|
||||||
if "admin" in line:
|
if not syslog_match:
|
||||||
illegal_resource = 'Possible Illegal Resource Access Attempt Was Made.'
|
continue
|
||||||
else:
|
|
||||||
illegal_resource = ''
|
|
||||||
|
|
||||||
if combined_webshells_pattern.search(line):
|
timestamp = syslog_match.group(1)
|
||||||
webshell_alert = 'Possible WebShell Attack Attempt Was Made.'
|
|
||||||
else:
|
# Extract IP:PORT
|
||||||
webshell_alert = ''
|
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({
|
parsed_entries.append({
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'ip_address': ip_address,
|
'ip_address': ip_address,
|
||||||
'http_method': http_method,
|
'http_method': http_method,
|
||||||
'requested_url': requested_url,
|
'requested_url': requested_url,
|
||||||
|
'status_code': status_code,
|
||||||
|
'frontend': frontend,
|
||||||
|
'backend': backend,
|
||||||
'xss_alert': xss_alert,
|
'xss_alert': xss_alert,
|
||||||
'sql_alert': sql_alert,
|
'sql_alert': sql_alert,
|
||||||
'put_method': put_method,
|
'put_method': put_method,
|
||||||
'illegal_resource': illegal_resource,
|
'illegal_resource': illegal_resource,
|
||||||
'webshell_alert': webshell_alert
|
'webshell_alert': webshell_alert,
|
||||||
})
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing line: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Log file not found: {log_file_path}")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading log file: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
return parsed_entries
|
return parsed_entries
|
||||||
@@ -8,12 +8,17 @@ edit_bp = Blueprint('edit', __name__)
|
|||||||
@requires_auth
|
@requires_auth
|
||||||
def edit_haproxy_config():
|
def edit_haproxy_config():
|
||||||
if request.method == 'POST':
|
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:
|
try:
|
||||||
with open('/etc/haproxy/haproxy.cfg', 'w') as f:
|
with open('/etc/haproxy/haproxy.cfg', 'w') as f:
|
||||||
f.write(edited_config)
|
f.write(edited_config)
|
||||||
|
print(f"[EDIT] Configuration saved successfully", flush=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"[EDIT] Error writing config: {e}", flush=True)
|
||||||
return render_template(
|
return render_template(
|
||||||
'edit.html',
|
'edit.html',
|
||||||
config_content=edited_config,
|
config_content=edited_config,
|
||||||
@@ -21,57 +26,80 @@ def edit_haproxy_config():
|
|||||||
check_level="danger"
|
check_level="danger"
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_check():
|
check_output = ""
|
||||||
|
check_level = "success"
|
||||||
|
|
||||||
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['haproxy', '-c', '-V', '-f', '/etc/haproxy/haproxy.cfg'],
|
['haproxy', '-c', '-V', '-f', '/etc/haproxy/haproxy.cfg'],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
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 result.returncode == 0:
|
||||||
if not out:
|
if not check_output:
|
||||||
out = "Configuration file is valid ✅"
|
check_output = "Configuration file is valid"
|
||||||
level = "success"
|
|
||||||
if "Warning" in out or "Warnings" in out:
|
|
||||||
level = "warning"
|
|
||||||
else:
|
|
||||||
if not out:
|
|
||||||
out = f"Check failed with return code {result.returncode}"
|
|
||||||
level = "danger"
|
|
||||||
|
|
||||||
return result.returncode, out, level
|
|
||||||
|
|
||||||
check_output = ""
|
|
||||||
check_level = "success"
|
check_level = "success"
|
||||||
|
|
||||||
if 'save_check' in request.form:
|
if "Warning" in check_output or "Warnings" in check_output:
|
||||||
_, check_output, check_level = run_check()
|
check_level = "warning"
|
||||||
|
check_output = f"⚠ {check_output}"
|
||||||
|
else:
|
||||||
|
check_output = f"✓ {check_output}"
|
||||||
|
|
||||||
elif 'save_reload' in request.form:
|
print(f"[EDIT] Config validation: SUCCESS", flush=True)
|
||||||
rc, out, level = run_check()
|
else:
|
||||||
check_output, check_level = out, level
|
if not check_output:
|
||||||
|
check_output = f"Check failed with return code {result.returncode}"
|
||||||
|
check_output = f"✗ {check_output}"
|
||||||
|
check_level = "danger"
|
||||||
|
print(f"[EDIT] Config validation: FAILED - {check_output}", flush=True)
|
||||||
|
|
||||||
if rc == 0:
|
except subprocess.TimeoutExpired:
|
||||||
|
check_output = "✗ Configuration check timed out"
|
||||||
|
check_level = "danger"
|
||||||
|
print(f"[EDIT] Config validation: TIMEOUT", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
check_output = f"✗ Error checking config: {e}"
|
||||||
|
check_level = "danger"
|
||||||
|
print(f"[EDIT] Config validation ERROR: {e}", flush=True)
|
||||||
|
|
||||||
|
if action == "save" and check_level == "success":
|
||||||
|
print(f"[EDIT] Attempting HAProxy restart...", flush=True)
|
||||||
try:
|
try:
|
||||||
supervisor_result = subprocess.run(
|
restart_result = subprocess.run(
|
||||||
['pkill', '-f', 'haproxy'],
|
['pkill', '-f', 'haproxy'],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
if supervisor_result.returncode == 0:
|
|
||||||
check_output += f"\n\nHAProxy Restarted:\n{supervisor_result.stdout}"
|
if restart_result.returncode == 0 or 'No such process' in restart_result.stdout:
|
||||||
|
check_output += "\n\n✓ HAProxy restart signal sent successfully"
|
||||||
|
check_output += "\n(supervisord will restart the process)"
|
||||||
|
print(f"[EDIT] HAProxy restart successful", flush=True)
|
||||||
else:
|
else:
|
||||||
check_output += (
|
check_output += f"\n\n⚠ Restart returned code {restart_result.returncode}"
|
||||||
f"\n\nRestart attempt returned {supervisor_result.returncode}:\n"
|
if restart_result.stdout:
|
||||||
f"{supervisor_result.stdout}"
|
check_output += f"\nOutput: {restart_result.stdout}"
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
check_output += f"\n\nRestart failed: {e}"
|
|
||||||
check_level = "warning"
|
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(
|
return render_template(
|
||||||
'edit.html',
|
'edit.html',
|
||||||
@@ -80,12 +108,19 @@ def edit_haproxy_config():
|
|||||||
check_level=check_level
|
check_level=check_level
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# GET request - load current config
|
||||||
try:
|
try:
|
||||||
with open('/etc/haproxy/haproxy.cfg', 'r') as f:
|
with open('/etc/haproxy/haproxy.cfg', 'r') as f:
|
||||||
config_content = f.read()
|
config_content = f.read()
|
||||||
|
print(f"[EDIT] Config loaded successfully ({len(config_content)} bytes)", flush=True)
|
||||||
except FileNotFoundError:
|
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:
|
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)
|
return render_template('edit.html', config_content=config_content)
|
||||||
|
|||||||
@@ -1,104 +1,252 @@
|
|||||||
from flask import Blueprint, render_template, request
|
from flask import Blueprint, render_template, request
|
||||||
from auth.auth_middleware import requires_auth # Updated import
|
import subprocess
|
||||||
from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends
|
from auth.auth_middleware import requires_auth
|
||||||
|
from utils.haproxy_config import update_haproxy_config, count_frontends_and_backends
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
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'])
|
@main_bp.route('/', methods=['GET', 'POST'])
|
||||||
@requires_auth
|
@requires_auth
|
||||||
def index():
|
def index():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
frontend_name = request.form['frontend_name']
|
# Frontend IP i port
|
||||||
frontend_ip = request.form['frontend_ip']
|
frontend_ip = request.form['frontend_ip']
|
||||||
frontend_port = request.form['frontend_port']
|
frontend_port = request.form['frontend_port']
|
||||||
|
frontend_hostname = request.form.get('frontend_hostname', '').strip()
|
||||||
|
|
||||||
lb_method = request.form['lb_method']
|
lb_method = request.form['lb_method']
|
||||||
protocol = request.form['protocol']
|
protocol = request.form['protocol']
|
||||||
backend_name = request.form['backend_name']
|
backend_name = request.form['backend_name']
|
||||||
|
|
||||||
|
# Header options
|
||||||
add_header = 'add_header' in request.form
|
add_header = 'add_header' in request.form
|
||||||
header_name = request.form.get('header_name', '') if add_header else ''
|
header_name = request.form.get('header_name', '') if add_header else ''
|
||||||
header_value = request.form.get('header_value', '') 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_names = request.form.getlist('backend_server_names[]')
|
||||||
backend_server_ips = request.form.getlist('backend_server_ips[]')
|
backend_server_ips = request.form.getlist('backend_server_ips[]')
|
||||||
backend_server_ports = request.form.getlist('backend_server_ports[]')
|
backend_server_ports = request.form.getlist('backend_server_ports[]')
|
||||||
backend_server_maxconns = request.form.getlist('backend_server_maxconns[]')
|
backend_server_maxconns = request.form.getlist('backend_server_maxconns[]')
|
||||||
|
|
||||||
is_acl = 'add_acl' in request.form
|
# Custom ACL
|
||||||
acl_name = request.form['acl'] if 'acl' in request.form else ''
|
add_custom_acl = 'add_custom_acl' in request.form
|
||||||
acl_action = request.form['acl_action'] if 'acl_action' in request.form else ''
|
custom_acl_name = request.form.get('custom_acl_name', '').strip() if add_custom_acl else ''
|
||||||
acl_backend_name = request.form['backend_name_acl'] if 'backend_name_acl' in request.form 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
|
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
|
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"]
|
# DOS Protection
|
||||||
limit_requests = request.form["limit_requests"]
|
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
|
forward_for = 'forward_for_check' in request.form
|
||||||
|
|
||||||
is_forbidden_path = 'add_acl_path' in request.form
|
# SQL Injection
|
||||||
forbidden_name = request.form["forbidden_name"]
|
sql_injection_check = 'sql_injection_check' in request.form
|
||||||
allowed_ip = request.form["allowed_ip"]
|
|
||||||
forbidden_path = request.form["forbidden_path"]
|
|
||||||
|
|
||||||
sql_injection_check = 'sql_injection_check' in request.form if 'sql_injection_check' in request.form else ''
|
# XSS
|
||||||
is_xss = 'xss_check' in request.form if 'xss_check' in request.form else ''
|
is_xss = 'xss_check' in request.form
|
||||||
is_remote_upload = 'remote_uploads_check' in request.form if 'remote_uploads_check' in request.form else ''
|
|
||||||
|
|
||||||
|
# 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
|
add_path_based = 'add_path_based' in request.form
|
||||||
redirect_domain_name = request.form["redirect_domain_name"]
|
redirect_domain_name = request.form.get('redirect_domain_name', '')
|
||||||
root_redirect = request.form["root_redirect"]
|
root_redirect = request.form.get('root_redirect', '')
|
||||||
redirect_to = request.form["redirect_to"]
|
redirect_to = request.form.get('redirect_to', '')
|
||||||
is_webshells = 'webshells_check' in request.form if 'webshells_check' in request.form else ''
|
|
||||||
|
|
||||||
# Combine backend server info into a list of tuples (name, ip, port, maxconns)
|
# 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 = []
|
backend_servers = []
|
||||||
for i in range(len(backend_server_ips)):
|
for i in range(len(backend_server_ips)):
|
||||||
name = backend_server_names[i] if i < len(backend_server_names) else f"server{i+1}"
|
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 ''
|
ip = backend_server_ips[i] if i < len(backend_server_ips) else ''
|
||||||
port = backend_server_ports[i] if i < len(backend_server_ports) 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
|
maxconn = backend_server_maxconns[i] if i < len(backend_server_maxconns) else None
|
||||||
|
if ip and port:
|
||||||
if ip and port: # Only add if we have IP and port
|
|
||||||
backend_servers.append((name, ip, port, maxconn))
|
backend_servers.append((name, ip, port, maxconn))
|
||||||
|
|
||||||
# Check if frontend or port already exists
|
# Health checks
|
||||||
if is_frontend_exist(frontend_name, frontend_ip, frontend_port):
|
|
||||||
return render_template('index.html', message="Frontend or Port already exists. Cannot add duplicate.")
|
|
||||||
|
|
||||||
# Get health check related fields if the protocol is HTTP
|
|
||||||
health_check = False
|
health_check = False
|
||||||
health_check_link = ""
|
health_check_link = ""
|
||||||
if protocol == 'http':
|
if protocol == 'http':
|
||||||
health_check = 'health_check' in request.form
|
health_check = 'health_check' in request.form
|
||||||
if health_check:
|
if health_check:
|
||||||
health_check_link = request.form['health_check_link']
|
health_check_link = request.form.get('health_check_link', '/')
|
||||||
|
|
||||||
health_check_tcp = False
|
health_check_tcp = False
|
||||||
if protocol == 'tcp':
|
if protocol == 'tcp':
|
||||||
health_check_tcp = 'health_check2' in request.form
|
health_check_tcp = 'health_check2' in request.form
|
||||||
|
|
||||||
# Get sticky session related fields
|
# Sticky session
|
||||||
sticky_session = False
|
sticky_session = False
|
||||||
sticky_session_type = ""
|
sticky_session_type = ""
|
||||||
if 'sticky_session' in request.form:
|
if 'sticky_session' in request.form:
|
||||||
sticky_session = True
|
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(
|
message = update_haproxy_config(
|
||||||
frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name,
|
frontend_name=frontend_name,
|
||||||
backend_servers, health_check, health_check_tcp, health_check_link, sticky_session,
|
frontend_ip=frontend_ip,
|
||||||
add_header, header_name, header_value, sticky_session_type, is_acl, acl_name,
|
frontend_port=frontend_port,
|
||||||
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
|
lb_method=lb_method,
|
||||||
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
|
protocol=protocol,
|
||||||
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
|
backend_name=backend_name,
|
||||||
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells
|
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
|
#!/bin/bash
|
||||||
|
git pull
|
||||||
docker compose down
|
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,23 +1,22 @@
|
|||||||
(() => {
|
(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// ===== HELPER FUNCTIONS =====
|
||||||
const $ = (sel, root = document) => root.querySelector(sel);
|
const $ = (sel, root = document) => root.querySelector(sel);
|
||||||
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||||
|
const toggle = (on, el) => el && el.classList.toggle('d-none', !on);
|
||||||
|
|
||||||
// SSL fields
|
// ===== SSL FIELDS =====
|
||||||
const sslCheckbox = $('#ssl_checkbox');
|
const sslCheckbox = $('#ssl_checkbox');
|
||||||
const sslFields = $('#ssl_fields');
|
const sslFields = $('#ssl_fields');
|
||||||
|
|
||||||
const toggle = (on, el) => el.classList.toggle('d-none', !on);
|
|
||||||
|
|
||||||
sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields));
|
sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields));
|
||||||
|
|
||||||
// DOS
|
// ===== DOS PROTECTION =====
|
||||||
const dosCheckbox = $('#add_dos');
|
const dosCheckbox = $('#add_dos');
|
||||||
const dosFields = $('#dos_fields');
|
const dosFields = $('#dos_fields');
|
||||||
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
|
dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields));
|
||||||
|
|
||||||
// HTTP only groups
|
// ===== PROTOCOL CHANGE (HTTP/TCP) =====
|
||||||
const protocolSelect = $('#protocol');
|
const protocolSelect = $('#protocol');
|
||||||
const httpGroups = $$('.http-only, #forbidden_acl_container');
|
const httpGroups = $$('.http-only, #forbidden_acl_container');
|
||||||
const httpToggles = [
|
const httpToggles = [
|
||||||
@@ -28,78 +27,56 @@
|
|||||||
$('#forward_for_check'),
|
$('#forward_for_check'),
|
||||||
$('#add_acl_path'),
|
$('#add_acl_path'),
|
||||||
$('#add_path_based'),
|
$('#add_path_based'),
|
||||||
|
$('#add_custom_acl'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const forbiddenFields = $('#forbidden_fields');
|
const forbiddenFields = $('#forbidden_fields');
|
||||||
const pathFields = $('#base_redirect_fields');
|
const pathFields = $('#base_redirect_fields');
|
||||||
|
|
||||||
const onProtocolChange = () => {
|
const onProtocolChange = () => {
|
||||||
const isHttp = protocolSelect?.value === 'http';
|
const isHttp = protocolSelect?.value === 'http';
|
||||||
httpGroups.forEach(el => toggle(isHttp, el));
|
httpGroups.forEach(el => toggle(isHttp, el));
|
||||||
|
|
||||||
if (!isHttp) {
|
if (!isHttp) {
|
||||||
// hide optional groups if protocol != http
|
[forbiddenFields, pathFields].forEach(el => toggle(false, el));
|
||||||
[forbiddenFields, pathFields].forEach(el => el && toggle(false, el));
|
httpToggles.forEach(input => {
|
||||||
httpToggles.forEach(input => { if (input) input.checked = false; });
|
if (input) input.checked = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
protocolSelect?.addEventListener('change', onProtocolChange);
|
protocolSelect?.addEventListener('change', onProtocolChange);
|
||||||
onProtocolChange();
|
onProtocolChange();
|
||||||
|
|
||||||
// ACL
|
// ===== BACKEND SSL REDIRECT =====
|
||||||
const aclCheckbox = $('#add_acl');
|
const backendSslCheckbox = $('#backend_ssl_redirect');
|
||||||
const aclFields = $('#acl_fields');
|
const backendSslFields = $('#backend_ssl_fields');
|
||||||
aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields));
|
|
||||||
|
|
||||||
// toggles that reveal their fields
|
backendSslCheckbox?.addEventListener('change', function() {
|
||||||
const bindToggle = (checkboxSel, targetSel) => {
|
toggle(this.checked, backendSslFields);
|
||||||
const cb = $(checkboxSel);
|
|
||||||
const target = $(targetSel);
|
|
||||||
cb?.addEventListener('change', () => toggle(cb.checked, target));
|
|
||||||
// initial
|
|
||||||
if (cb && target) toggle(cb.checked, target);
|
|
||||||
};
|
|
||||||
bindToggle('#add_path_based', '#base_redirect_fields');
|
|
||||||
bindToggle('#add_acl_path', '#forbidden_fields');
|
|
||||||
|
|
||||||
// Backend rows
|
|
||||||
let serverCount = 1;
|
|
||||||
const container = $('#backend_servers_container');
|
|
||||||
const addBtn = $('#add_backend_btn');
|
|
||||||
|
|
||||||
const createRow = () => {
|
|
||||||
serverCount++;
|
|
||||||
const row = document.createElement('div');
|
|
||||||
row.className = 'row g-3 backend-server-row mt-1';
|
|
||||||
row.innerHTML = `
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label" for="name${serverCount}">Nazwa serwera</label>
|
|
||||||
<input type="text" id="name${serverCount}" class="form-control" name="backend_server_names[]" placeholder="server${serverCount}" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label" for="ip${serverCount}">IP</label>
|
|
||||||
<input type="text" id="ip${serverCount}" class="form-control" name="backend_server_ips[]" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label" for="port${serverCount}">Port</label>
|
|
||||||
<input type="number" id="port${serverCount}" class="form-control" name="backend_server_ports[]" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label" for="maxconn${serverCount}">MaxConn</label>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<input type="number" id="maxconn${serverCount}" class="form-control" name="backend_server_maxconns[]">
|
|
||||||
<button type="button" class="btn btn-danger" title="Usuń">Usuń</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
row.querySelector('button.btn-danger')?.addEventListener('click', () => {
|
|
||||||
const rows = $$('.backend-server-row');
|
|
||||||
if (rows.length > 1) row.remove();
|
|
||||||
else alert('Musi istnieć co najmniej jeden backend.');
|
|
||||||
});
|
});
|
||||||
return row;
|
|
||||||
};
|
|
||||||
addBtn?.addEventListener('click', () => container?.appendChild(createRow()));
|
|
||||||
|
|
||||||
// auto dismiss alerts
|
// ===== CUSTOM ACL (Main Toggle) =====
|
||||||
setTimeout(() => $$('.alert').forEach(a => {
|
const customAclCheckbox = $('#add_custom_acl');
|
||||||
if (typeof bootstrap !== 'undefined') new bootstrap.Alert(a).close();
|
const customAclFields = $('#custom_acl_fields');
|
||||||
}), 5000);
|
|
||||||
|
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
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
pidfile=/var/run/supervisord.pid
|
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]
|
[program:haproxy]
|
||||||
command=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg
|
command=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg
|
||||||
autostart=true
|
autostart=true
|
||||||
@@ -14,6 +24,8 @@ stdout_logfile=/var/log/haproxy.log
|
|||||||
priority=100
|
priority=100
|
||||||
stopasgroup=true
|
stopasgroup=true
|
||||||
killasgroup=true
|
killasgroup=true
|
||||||
|
startsecs=10
|
||||||
|
stopwaitsecs=10
|
||||||
|
|
||||||
[program:flask_app]
|
[program:flask_app]
|
||||||
command=python /app/app.py
|
command=python /app/app.py
|
||||||
@@ -26,10 +38,3 @@ priority=999
|
|||||||
environment=FLASK_APP=/app/app.py,FLASK_ENV=production,PYTHONUNBUFFERED=1
|
environment=FLASK_APP=/app/app.py,FLASK_ENV=production,PYTHONUNBUFFERED=1
|
||||||
startsecs=10
|
startsecs=10
|
||||||
stopasgroup=true
|
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>
|
<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 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/main.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/edit.css') }}">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
<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" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "" %}
|
|
||||||
{% block title %}HAProxy • Edit{% endblock %}
|
{% set active_page = "edit" %}
|
||||||
{% 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 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 %}
|
{% block content %}
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-body">
|
<!-- CodeMirror CSS -->
|
||||||
<h4 class="mb-3 text-muted">Edit HAProxy configuration</h4>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
||||||
<form method="POST" novalidate>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/material-darker.min.css">
|
||||||
<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 %}
|
{% if check_output %}
|
||||||
<div class="alert alert-{{ check_level|default('success') }}" role="alert">
|
<div class="alert alert-{{ check_level|default('info') }} alert-dismissible fade show" role="alert">
|
||||||
<pre class="mb-0">{{ check_output }}</pre>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
{% block page_js %}
|
<!-- CodeMirror JS -->
|
||||||
<script src="{{ url_for('static', filename='js/edit.js') }}"></script>
|
<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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,220 +1,467 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% set active_page = "index" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="card shadow-sm">
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="/" id="fe-be-form" novalidate>
|
<h5 class="card-title text-primary">{{ frontend_count|default(0) }}</h5>
|
||||||
<h5 class="mb-3"><i class="fas fa-globe me-2"></i>New frontend</h5>
|
<p class="card-text"><i class="bi bi-diagram-2"></i> Frontends</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-success">{{ backend_count|default(0) }}</h5>
|
||||||
|
<p class="card-text"><i class="bi bi-hdd-rack"></i> Backends</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-warning">{{ acl_count|default(0) }}</h5>
|
||||||
|
<p class="card-text"><i class="bi bi-shield"></i> ACLs</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-info">L7: {{ layer7_count|default(0) }} / L4: {{ layer4_count|default(0) }}</h5>
|
||||||
|
<p class="card-text"><i class="bi bi-layers"></i> Layers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div class="alert {% if 'already exists' in message %}alert-danger{% else %}alert-success{% endif %} alert-dismissible" role="alert">
|
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-{% if message_type == 'success' %}check-circle{% elif message_type == 'danger' %}exclamation-circle{% else %}info-circle{% endif %} me-2"></i>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="row g-3">
|
<form method="post" class="needs-validation">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add New Configuration</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<!-- FRONTEND SECTION -->
|
||||||
|
<h6 class="text-primary mb-3"><i class="bi bi-hdd-network me-2"></i>Frontend Configuration</h6>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label" for="frontend_name">Name</label>
|
<label for="frontend_ip" class="form-label">Listener IP</label>
|
||||||
<input type="text" class="form-control" name="frontend_name" id="frontend_name" required>
|
<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>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label" for="frontend_ip">IP</label>
|
<label for="frontend_port" class="form-label">Listener Port</label>
|
||||||
<input type="text" class="form-control" name="frontend_ip" id="frontend_ip" required>
|
<input type="number" class="form-control" id="frontend_port" name="frontend_port"
|
||||||
|
placeholder="443" value="443" min="1" max="65535" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-4">
|
||||||
<label class="form-label" for="frontend_port">Port</label>
|
<label for="frontend_hostname" class="form-label">Frontend Hostname</label>
|
||||||
<input type="number" class="form-control" name="frontend_port" id="frontend_port" required>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3">
|
<div class="row g-3 mb-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">
|
<div class="col-md-6">
|
||||||
<label class="form-label" for="ssl_cert_path">Certificate path (put in /ssl/)</label>
|
<label for="protocol" class="form-label">Protocol</label>
|
||||||
<input type="text" id="ssl_cert_path" class="form-control" name="ssl_cert_path">
|
<select class="form-select" id="protocol" name="protocol" required>
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="tcp">TCP</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 d-flex align-items-end">
|
<div class="col-md-6">
|
||||||
<div class="form-check">
|
<label for="lb_method" class="form-label">Load Balancing Method</label>
|
||||||
<input type="checkbox" class="form-check-input" id="ssl_redirect_checkbox" name="ssl_redirect_checkbox">
|
<select class="form-select" id="lb_method" name="lb_method" required>
|
||||||
<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="roundrobin">Round Robin</option>
|
||||||
<option value="leastconn">Least Connections</option>
|
<option value="leastconn">Least Connections</option>
|
||||||
<option value="source">Source</option>
|
<option value="source">Source IP Hash</option>
|
||||||
<option value="wrr">WRR</option>
|
<option value="uri">URI Hash</option>
|
||||||
<option value="wlc">WLC</option>
|
<option value="static-rr">Static Round Robin (WRR)</option>
|
||||||
<option value="random">Random</option>
|
<option value="no-lb">No Load Balancing (single host)</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3">
|
<!-- SSL Section -->
|
||||||
<input type="checkbox" class="form-check-input" name="add_dos" id="add_dos">
|
<div class="row g-3 mb-3">
|
||||||
<label class="form-check-label" for="add_dos"><i class="fas fa-shield-alt me-2"></i>DOS protection</label>
|
<div class="col-md-12">
|
||||||
</div>
|
<div class="form-check form-switch">
|
||||||
<div class="row g-3 mt-1 d-none" id="dos_fields">
|
<input class="form-check-input" type="checkbox" id="ssl_checkbox" name="ssl_checkbox">
|
||||||
<div class="col-md-6">
|
<label class="form-check-label" for="ssl_checkbox">
|
||||||
<label class="form-label" for="limit_requests">Limit (np. 20)</label>
|
<i class="bi bi-lock me-1"></i>Use SSL (HTTPS)
|
||||||
<input type="text" class="form-control" name="limit_requests" id="limit_requests">
|
</label>
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label" for="ban_duration">Ban (np. 15s)</label>
|
|
||||||
<input type="text" class="form-control" name="ban_duration" id="ban_duration">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3 mt-2 d-none http-only" id="http_extras">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="sql_injection_check" name="sql_injection_check">
|
|
||||||
<label class="form-check-label" for="sql_injection_check"><i class="fas fa-shield-alt me-2"></i>SQLi</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="xss_check" name="xss_check">
|
|
||||||
<label class="form-check-label" for="xss_check"><i class="fas fa-shield-alt me-2"></i>XSS</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="remote_uploads_check" name="remote_uploads_check">
|
|
||||||
<label class="form-check-label" for="remote_uploads_check"><i class="fas fa-shield-alt me-2"></i>Remote uploads</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="webshells_check" name="webshells_check">
|
|
||||||
<label class="form-check-label" for="webshells_check"><i class="fas fa-shield-alt me-2"></i>Webshells</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="forward_for_check" name="forward_for_check">
|
|
||||||
<label class="form-check-label" for="forward_for_check"><i class="fas fa-network-wired me-2"></i>forwardfor</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3">
|
<div class="row g-3 mb-3 d-none" id="ssl_fields">
|
||||||
<input type="checkbox" class="form-check-input" name="add_acl" id="add_acl">
|
<div class="col-md-12">
|
||||||
<label class="form-check-label" for="add_acl"><i class="fas fa-user-lock me-2"></i>ACL for frontend</label>
|
<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>
|
||||||
<div class="row g-3 mt-1 d-none" id="acl_fields">
|
<div class="col-md-12">
|
||||||
<div class="col-md-4">
|
<div class="form-check">
|
||||||
<label class="form-label" for="acl">ACL</label>
|
<input class="form-check-input" type="checkbox" id="ssl_redirect_checkbox"
|
||||||
<input type="text" class="form-control" name="acl" id="acl" placeholder="acl_name">
|
name="ssl_redirect_checkbox">
|
||||||
|
<label class="form-check-label" for="ssl_redirect_checkbox">
|
||||||
|
Redirect HTTP to HTTPS
|
||||||
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3 http-only d-none" id="forbidden_acl_container">
|
<!-- Backend SSL Redirect -->
|
||||||
<input type="checkbox" class="form-check-input" name="add_acl_path" id="add_acl_path">
|
<div class="row g-3 mb-3">
|
||||||
<label class="form-check-label" for="add_acl_path"><i class="fas fa-ban me-2"></i>Block path</label>
|
<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 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>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3 http-only d-none" id="path_based_container">
|
<div class="row g-3 mb-3 d-none" id="backend_ssl_fields">
|
||||||
<input type="checkbox" class="form-check-input" name="add_path_based" id="add_path_based">
|
<div class="col-md-12">
|
||||||
<label class="form-check-label" for="add_path_based"><i class="fas fa-arrow-circle-right me-2"></i>Path-based redirect</label>
|
<label for="ssl_redirect_backend_name" class="form-label">Redirect Backend Name</label>
|
||||||
</div>
|
<input type="text" class="form-control" id="ssl_redirect_backend_name"
|
||||||
<div class="row g-3 mt-1 d-none" id="base_redirect_fields">
|
name="ssl_redirect_backend_name" placeholder="e.g. redirect">
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<h5 class="mb-3"><i class="fas fa-sitemap me-2"></i>Backend pool</h5>
|
<!-- BACKEND SECTION -->
|
||||||
<div class="row g-3">
|
<h6 class="text-primary mb-3"><i class="bi bi-hdd-rack me-2"></i>Backend Configuration</h6>
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label" for="backend_name">Backend name</label>
|
<div class="row g-3 mb-3">
|
||||||
<input type="text" class="form-control" name="backend_name" id="backend_name" required>
|
<div class="col-md-12">
|
||||||
|
<label for="backend_name" class="form-label">Backend Name</label>
|
||||||
|
<input type="text" class="form-control" id="backend_name" name="backend_name"
|
||||||
|
placeholder="e.g. be_web" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="backend_servers_container" class="mt-3">
|
<!-- Backend servers -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Backend Servers</label>
|
||||||
|
<div id="backend_servers_container">
|
||||||
<div class="row g-3 backend-server-row">
|
<div class="row g-3 backend-server-row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" for="name1">Server name</label>
|
<input type="text" class="form-control" name="backend_server_names[]"
|
||||||
<input type="text" id="name1" class="form-control" name="backend_server_names[]" placeholder="server1" required>
|
placeholder="server1" value="server1" required>
|
||||||
</div>
|
</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">
|
<div class="col-md-3">
|
||||||
<label class="form-label" for="ip1">IP</label>
|
<label for="custom_acl_name" class="form-label">ACL Name</label>
|
||||||
<input type="text" id="ip1" class="form-control" name="backend_server_ips[]" required>
|
<input type="text" class="form-control" id="custom_acl_name" name="custom_acl_name"
|
||||||
|
placeholder="e.g. is_admin_path">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" for="port1">Port</label>
|
<label for="custom_acl_type" class="form-label">Rule Type</label>
|
||||||
<input type="number" id="port1" class="form-control" name="backend_server_ports[]" required>
|
<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>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" for="maxconn1">MaxConn</label>
|
<label for="custom_acl_value" class="form-label">Rule Value</label>
|
||||||
<input type="number" id="maxconn1" class="form-control" name="backend_server_maxconns[]">
|
<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>
|
||||||
</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>
|
||||||
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
{% block page_js %}
|
|
||||||
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/form.js') }}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,50 +1,211 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "" %}
|
|
||||||
|
{% set active_page = "logs" %}
|
||||||
|
|
||||||
{% block title %}HAProxy • Logs{% endblock %}
|
{% block title %}HAProxy • Logs{% endblock %}
|
||||||
{% block breadcrumb %}<nav aria-label="breadcrumb" class="mb-3"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item active" aria-current="page">Logi</li></ol></nav>{% endblock %}
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
<nav aria-label="breadcrumb" class="mb-3">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Access Logs</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3 class="mb-4" id="status_header">Status 403 Forbidden</h3>
|
|
||||||
{% if entries %}
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="vstack gap-3">
|
<div class="card-header bg-primary text-white">
|
||||||
{% for entry in entries %}
|
<h5 class="mb-0"><i class="bi bi-file-text me-2"></i>HAProxy Access Logs</h5>
|
||||||
<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>
|
||||||
<div class="col-md-6">
|
<div class="card-body">
|
||||||
|
|
||||||
|
{% if error_message %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Warning:</strong> {{ error_message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if logs and logs|length > 0 %}
|
||||||
|
<div class="row mb-3 g-2">
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" class="form-control form-control-sm" id="filter_ip" placeholder="Filter by IP">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm" id="filter_status" style="width: auto;">
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="2">2xx (Success)</option>
|
||||||
|
<option value="3">3xx (Redirect)</option>
|
||||||
|
<option value="4">4xx (Client Error)</option>
|
||||||
|
<option value="5">5xx (Server Error)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm" id="filter_method" style="width: auto;">
|
||||||
|
<option value="">All Methods</option>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_threats" checked>
|
||||||
|
<label class="form-check-label" for="filter_threats" style="margin-top: 5px;">
|
||||||
|
Show Threats
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_hide_stats" checked>
|
||||||
|
<label class="form-check-label" for="filter_hide_stats" style="margin-top: 5px;">
|
||||||
|
Hide /stats
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<button class="btn btn-sm btn-secondary" id="reset_filters">Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3 g-2">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center" style="font-size: 0.9rem;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="text-muted small">Total</div>
|
||||||
|
<strong id="stat_total">{{ logs|length }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center text-danger" style="font-size: 0.9rem;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="text-muted small">Threats</div>
|
||||||
|
<strong id="stat_threats">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center text-success" style="font-size: 0.9rem;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="text-muted small">2xx</div>
|
||||||
|
<strong id="stat_2xx">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center text-warning" style="font-size: 0.9rem;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="text-muted small">4xx</div>
|
||||||
|
<strong id="stat_4xx">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center text-danger" style="font-size: 0.9rem;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="text-muted small">5xx</div>
|
||||||
|
<strong id="stat_5xx">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card text-center" style="font-size: 0.9rem;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="text-muted small">Unique IPs</div>
|
||||||
|
<strong id="stat_ips">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>HTTP Method</th>
|
||||||
|
<th>Requested URL</th>
|
||||||
|
<th>Status Code</th>
|
||||||
|
<th>Alerts</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logs_table">
|
||||||
|
{% for entry in logs %}
|
||||||
|
<tr class="log-row"
|
||||||
|
data-ip="{{ entry['ip_address'] }}"
|
||||||
|
data-status="{{ entry['status_code'] }}"
|
||||||
|
data-method="{{ entry['http_method'] }}"
|
||||||
|
data-threats="{% if entry['xss_alert'] or entry['sql_alert'] or entry['put_method'] or entry['webshell_alert'] or entry['illegal_resource'] %}1{% else %}0{% endif %}">
|
||||||
|
<td>{{ entry['timestamp'] }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ entry['ip_address'] }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-primary">{{ entry['http_method'] }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-truncate" style="max-width: 300px;" title="{{ entry['requested_url'] }}">
|
||||||
|
{{ entry['requested_url'] }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if entry['status_code']|int >= 200 and entry['status_code']|int < 300 %}bg-success{% elif entry['status_code']|int >= 300 and entry['status_code']|int < 400 %}bg-secondary{% elif entry['status_code']|int >= 400 and entry['status_code']|int < 500 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||||
|
{{ entry['status_code'] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
{% if entry['xss_alert'] %}
|
{% 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>
|
<span class="badge bg-danger">XSS</span>
|
||||||
<div id="xssCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['xss_alert'] }}</pre></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if entry['sql_alert'] %}
|
{% 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>
|
<span class="badge bg-danger">SQL</span>
|
||||||
<div id="sqlCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-warning">{{ entry['sql_alert'] }}</pre></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if entry['put_method'] %}
|
{% 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>
|
<span class="badge bg-warning">PUT</span>
|
||||||
<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 %}
|
{% endif %}
|
||||||
{% if entry['webshell_alert'] %}
|
{% 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>
|
<span class="badge bg-danger">Webshell</span>
|
||||||
<div id="webshellCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['webshell_alert'] }}</pre></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% if entry['illegal_resource'] %}
|
||||||
</div>
|
<span class="badge bg-warning">403</span>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% 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>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info"><i class="bi bi-info-circle me-1"></i>No data.</div>
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<h4 class="alert-heading"><i class="bi bi-exclamation-circle me-2"></i>No logs available</h4>
|
||||||
|
<hr>
|
||||||
|
<p class="mb-2"><strong>Possible reasons:</strong></p>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Log file does not exist or is not readable</li>
|
||||||
|
<li>HAProxy is not configured to log requests</li>
|
||||||
|
<li>Log file path is incorrect in configuration</li>
|
||||||
|
<li>No requests have been processed yet</li>
|
||||||
|
</ul>
|
||||||
|
<hr class="my-2">
|
||||||
|
<p class="small text-muted mb-0">Check HAProxy configuration and log file permissions.</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,33 +2,86 @@ import os
|
|||||||
|
|
||||||
HAPROXY_CFG = '/etc/haproxy/haproxy.cfg'
|
HAPROXY_CFG = '/etc/haproxy/haproxy.cfg'
|
||||||
|
|
||||||
def is_frontend_exist(frontend_name, frontend_ip, frontend_port):
|
def sanitize_name(name):
|
||||||
"""Check if frontend with given name, IP and port already exists"""
|
"""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):
|
if not os.path.exists(HAPROXY_CFG):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(HAPROXY_CFG, 'r') as haproxy_cfg:
|
with open(HAPROXY_CFG, 'r') as f:
|
||||||
frontend_found = False
|
lines = f.readlines()
|
||||||
for line in haproxy_cfg:
|
|
||||||
if line.strip().startswith('frontend'):
|
# Znajdź frontend
|
||||||
_, existing_frontend_name = line.strip().split(' ', 1)
|
frontend_idx = -1
|
||||||
if existing_frontend_name.strip() == frontend_name:
|
for i, line in enumerate(lines):
|
||||||
frontend_found = True
|
if 'frontend' in line and frontend_name in line:
|
||||||
else:
|
frontend_idx = i
|
||||||
frontend_found = False
|
break
|
||||||
elif frontend_found and line.strip().startswith('bind'):
|
|
||||||
_, bind_info = line.strip().split(' ', 1)
|
if frontend_idx == -1:
|
||||||
existing_ip, existing_port = bind_info.split(':', 1)
|
return False
|
||||||
if existing_ip.strip() == frontend_ip and existing_port.strip() == frontend_port:
|
|
||||||
|
# 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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[HAPROXY_CONFIG] Error checking frontend: {e}", flush=True)
|
print(f"[HAPROXY_CONFIG] Error adding ACL: {e}", flush=True)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_backend_exist(backend_name):
|
def is_backend_exist(backend_name):
|
||||||
"""Check if backend with given name already exists"""
|
|
||||||
if not os.path.exists(HAPROXY_CFG):
|
if not os.path.exists(HAPROXY_CFG):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -46,7 +99,6 @@ def is_backend_exist(backend_name):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def count_frontends_and_backends():
|
def count_frontends_and_backends():
|
||||||
"""Count frontends, backends, ACLs and layer types"""
|
|
||||||
if not os.path.exists(HAPROXY_CFG):
|
if not os.path.exists(HAPROXY_CFG):
|
||||||
return 0, 0, 0, 0, 0
|
return 0, 0, 0, 0, 0
|
||||||
|
|
||||||
@@ -84,37 +136,112 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
|
|||||||
acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos,
|
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,
|
ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name,
|
||||||
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
|
allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload,
|
||||||
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells):
|
add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells,
|
||||||
|
del_server_header=False, backend_ssl_redirect=False, ssl_redirect_backend_name='',
|
||||||
|
ssl_redirect_port='80', frontend_hostname='', add_custom_acl=False,
|
||||||
|
custom_acl_name='', custom_acl_type='path_beg', custom_acl_value='',
|
||||||
|
custom_acl_action='route', custom_acl_backend='', custom_acl_redirect_url=''):
|
||||||
|
|
||||||
# Ensure directory exists
|
|
||||||
os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True)
|
os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True)
|
||||||
|
|
||||||
if is_backend_exist(backend_name):
|
unique_backend_name = f"{backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else backend_name
|
||||||
return f"Backend {backend_name} already exists. Cannot add duplicate."
|
|
||||||
|
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:
|
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:
|
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
|
||||||
haproxy_cfg.write(f"\nfrontend {frontend_name}\n")
|
# ===== BACKEND =====
|
||||||
|
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
|
||||||
|
|
||||||
if is_frontend_exist(frontend_name, frontend_ip, frontend_port):
|
if not is_no_lb:
|
||||||
return "Frontend or Port already exists. Cannot add duplicate."
|
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||||
|
|
||||||
haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}")
|
if sticky_session and not is_no_lb:
|
||||||
|
if sticky_session_type == "cookie":
|
||||||
|
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
|
||||||
|
elif sticky_session_type == "source":
|
||||||
|
haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n")
|
||||||
|
haproxy_cfg.write(f" stick on src\n")
|
||||||
|
|
||||||
if use_ssl:
|
if health_check and protocol == 'http':
|
||||||
haproxy_cfg.write(f" ssl crt {ssl_cert_path}")
|
haproxy_cfg.write(f" option httpchk GET {health_check_link}\n")
|
||||||
haproxy_cfg.write("\n")
|
elif health_check_tcp and protocol == 'tcp':
|
||||||
|
haproxy_cfg.write(f" option tcp-check\n")
|
||||||
|
|
||||||
if https_redirect:
|
if add_header:
|
||||||
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
|
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:
|
if forward_for:
|
||||||
haproxy_cfg.write(f" option forwardfor\n")
|
haproxy_cfg.write(f" option forwardfor\n")
|
||||||
|
|
||||||
haproxy_cfg.write(f" mode {protocol}\n")
|
# Add servers
|
||||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
for server_name, server_ip, server_port, maxconn in backend_servers:
|
||||||
|
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
|
||||||
|
|
||||||
# Add protection rules
|
if health_check and protocol == 'http':
|
||||||
|
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n")
|
||||||
|
else:
|
||||||
|
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
|
||||||
|
|
||||||
|
# Dodaj ACL do istniejącego frontendu
|
||||||
|
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"is_{unique_backend_name}"
|
||||||
|
add_acl_to_frontend(existing_frontend, acl_name_sanitized, frontend_hostname or 'localhost', unique_backend_name)
|
||||||
|
|
||||||
|
return f"Backend added to existing frontend"
|
||||||
|
|
||||||
|
# ===== TWORZENIE NOWEGO FRONTENDU (GENERYCZNE NAZWY) =====
|
||||||
|
# Generuj generyczną nazwę frontendu
|
||||||
|
generic_frontend_name = f"https_frontend" if use_ssl else f"http_frontend"
|
||||||
|
generic_http_redirect_name = f"http_redirect_frontend"
|
||||||
|
|
||||||
|
print(f"[HAPROXY] Creating new frontend '{generic_frontend_name}' at {frontend_ip}:{frontend_port}", flush=True)
|
||||||
|
|
||||||
|
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
|
||||||
|
# ===== PRIMARY FRONTEND (GENERIC NAME) =====
|
||||||
|
haproxy_cfg.write(f"\nfrontend {generic_frontend_name}\n")
|
||||||
|
haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}")
|
||||||
|
|
||||||
|
if use_ssl:
|
||||||
|
haproxy_cfg.write(f" ssl crt {ssl_cert_path}")
|
||||||
|
|
||||||
|
haproxy_cfg.write("\n")
|
||||||
|
|
||||||
|
# Headers zaraz po BIND/CERT
|
||||||
|
haproxy_cfg.write(f" http-request set-header X-Forwarded-For %[src]\n")
|
||||||
|
if use_ssl:
|
||||||
|
haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto https\n")
|
||||||
|
else:
|
||||||
|
haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto http\n")
|
||||||
|
|
||||||
|
haproxy_cfg.write(f" mode {protocol}\n")
|
||||||
|
|
||||||
|
# ACL dla pierwszego vhost
|
||||||
|
acl_name_sanitized = None
|
||||||
|
if frontend_hostname:
|
||||||
|
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}"
|
||||||
|
haproxy_cfg.write(f" acl {acl_name_sanitized} hdr(host) -i {frontend_hostname}\n")
|
||||||
|
|
||||||
|
if not is_no_lb:
|
||||||
|
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||||
|
if forward_for:
|
||||||
|
haproxy_cfg.write(f" option forwardfor\n")
|
||||||
|
|
||||||
|
# Protections
|
||||||
if is_dos:
|
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" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n")
|
||||||
haproxy_cfg.write(f" http-request track-sc0 src\n")
|
haproxy_cfg.write(f" 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")
|
haproxy_cfg.write(f" http-request silent-drop if abuse\n")
|
||||||
|
|
||||||
if sql_injection_check:
|
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_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 is_long_uri path_len gt 400\n")
|
||||||
haproxy_cfg.write(" acl semicolon_path path_reg -i ^.*;.*\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(" 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" 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
|
if del_server_header:
|
||||||
haproxy_cfg.write(f"\nbackend {backend_name}\n")
|
haproxy_cfg.write(f" http-response del-header Server\n")
|
||||||
|
|
||||||
|
# Backend routing
|
||||||
|
if acl_name_sanitized:
|
||||||
|
haproxy_cfg.write(f" use_backend {unique_backend_name} if {acl_name_sanitized}\n")
|
||||||
|
else:
|
||||||
|
haproxy_cfg.write(f" default_backend {unique_backend_name}\n")
|
||||||
|
|
||||||
|
# ===== BACKEND =====
|
||||||
|
haproxy_cfg.write(f"\nbackend {unique_backend_name}\n")
|
||||||
|
|
||||||
|
if not is_no_lb:
|
||||||
haproxy_cfg.write(f" balance {lb_method}\n")
|
haproxy_cfg.write(f" balance {lb_method}\n")
|
||||||
|
|
||||||
if sticky_session:
|
if sticky_session and not is_no_lb:
|
||||||
if sticky_session_type == "cookie":
|
if sticky_session_type == "cookie":
|
||||||
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
|
haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n")
|
||||||
elif sticky_session_type == "source":
|
elif sticky_session_type == "source":
|
||||||
@@ -160,15 +298,52 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
|
|||||||
if add_header:
|
if add_header:
|
||||||
haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n")
|
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:
|
for server_name, server_ip, server_port, maxconn in backend_servers:
|
||||||
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
|
maxconn_str = f" maxconn {maxconn}" if maxconn else ""
|
||||||
|
|
||||||
if health_check and protocol == 'http':
|
if health_check and protocol == 'http':
|
||||||
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n")
|
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n")
|
||||||
else:
|
else:
|
||||||
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
|
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
|
||||||
|
|
||||||
|
# ===== REDIRECT HTTP -> HTTPS (GENERIC NAME) =====
|
||||||
|
if backend_ssl_redirect and ssl_redirect_backend_name:
|
||||||
|
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else ssl_redirect_backend_name
|
||||||
|
|
||||||
|
# Check if HTTP redirect frontend exists
|
||||||
|
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
|
||||||
|
|
||||||
|
if not existing_http_frontend:
|
||||||
|
# Utwórz nowy HTTP redirect frontend (generic name)
|
||||||
|
haproxy_cfg.write(f"\nfrontend {generic_http_redirect_name}\n")
|
||||||
|
haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n")
|
||||||
|
haproxy_cfg.write(f" mode http\n")
|
||||||
|
|
||||||
|
if frontend_hostname:
|
||||||
|
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
|
||||||
|
haproxy_cfg.write(f" acl {acl_name_redirect} hdr(host) -i {frontend_hostname}\n")
|
||||||
|
haproxy_cfg.write(f" use_backend {unique_redirect_backend_name} if {acl_name_redirect}\n")
|
||||||
|
else:
|
||||||
|
haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n")
|
||||||
|
else:
|
||||||
|
# Dodaj ACL do istniejącego HTTP frontendu
|
||||||
|
if frontend_hostname:
|
||||||
|
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
|
||||||
|
add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name)
|
||||||
|
|
||||||
|
# Redirect backend
|
||||||
|
haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n")
|
||||||
|
haproxy_cfg.write(f" mode http\n")
|
||||||
|
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
|
||||||
|
|
||||||
return "Configuration updated successfully!"
|
return "Configuration updated successfully!"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[HAPROXY_CONFIG] Error updating config: {e}", flush=True)
|
print(f"[HAPROXY_CONFIG] Error updating config: {e}", flush=True)
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import requests
|
import requests
|
||||||
import csv
|
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():
|
def fetch_haproxy_stats():
|
||||||
try:
|
try:
|
||||||
@@ -13,18 +13,58 @@ def fetch_haproxy_stats():
|
|||||||
|
|
||||||
def parse_haproxy_stats(stats_data):
|
def parse_haproxy_stats(stats_data):
|
||||||
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()]
|
||||||
next(reader)
|
if not lines:
|
||||||
for row in reader:
|
return data
|
||||||
if row['svname'] != 'BACKEND':
|
|
||||||
data.append({
|
header_row = lines[0].replace('# ', '')
|
||||||
'frontend_name': row['pxname'],
|
|
||||||
'server_name': row['svname'],
|
# Parse CSV
|
||||||
'4xx_errors': row['hrsp_4xx'],
|
reader = csv.DictReader(lines, fieldnames=header_row.split(','))
|
||||||
'5xx_errors': row['hrsp_5xx'],
|
next(reader)
|
||||||
'bytes_in_mb': f'{float(row["bin"]) / (1024 * 1024):.2f}',
|
|
||||||
'bytes_out_mb': f'{float(row["bout"]) / (1024 * 1024):.2f}',
|
for row in reader:
|
||||||
'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
|
return data
|
||||||
Reference in New Issue
Block a user