diff --git a/.gitignore b/.gitignore index a41dd9f..be16eda 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ venv -logs/* \ No newline at end of file +logs/* +__pycache__ +config/* \ No newline at end of file diff --git a/app.py b/app.py index acc9278..a4b7f49 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ + from flask import Flask, render_template, render_template_string import configparser import ssl @@ -6,10 +7,38 @@ from routes.edit_routes import edit_bp from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats from auth.auth_middleware import setup_auth from log_parser import parse_log_file +import os +import sys + app = Flask(__name__) +# Uniwersalne ścieżki - sprawdzaj obie +CONFIG_DIR_DOCKER = '/etc/haproxy-configurator' +CONFIG_DIR_LOCAL = './config' +CONFIG_DIR_ENV = os.environ.get('CONFIG_DIR', None) + +# Określ która ścieżka istnieje +if CONFIG_DIR_ENV and os.path.exists(CONFIG_DIR_ENV): + CONFIG_DIR = CONFIG_DIR_ENV +elif os.path.exists(CONFIG_DIR_DOCKER): + CONFIG_DIR = CONFIG_DIR_DOCKER +elif os.path.exists(CONFIG_DIR_LOCAL): + CONFIG_DIR = CONFIG_DIR_LOCAL +else: + CONFIG_DIR = CONFIG_DIR_DOCKER # Fallback + +AUTH_CFG = os.path.join(CONFIG_DIR, 'auth', 'auth.cfg') +SSL_INI = os.path.join(CONFIG_DIR, 'ssl.ini') + +# Create directories +os.makedirs(os.path.dirname(AUTH_CFG), exist_ok=True) +os.makedirs(os.path.dirname(SSL_INI), exist_ok=True) + # Load basic auth credentials +BASIC_AUTH_USERNAME = "admin" +BASIC_AUTH_PASSWORD = "admin" + try: auth_config = configparser.ConfigParser() auth_config.read(AUTH_CFG) @@ -24,20 +53,50 @@ except Exception as e: BASIC_AUTH_USERNAME = "admin" BASIC_AUTH_PASSWORD = "admin" + # Register blueprints app.register_blueprint(main_bp) app.register_blueprint(edit_bp) -# Setup authentication (placeholder, not currently used) + +# Setup authentication setup_auth(app) -# SSL Configuration -config2 = configparser.ConfigParser() -config2.read('/etc/haproxy-configurator/ssl.ini') -certificate_path = config2.get('ssl', 'certificate_path') -private_key_path = config2.get('ssl', 'private_key_path') -ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) -ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path) + +# SSL Configuration - Z ERROR HANDLINGIEM +certificate_path = None +private_key_path = None +ssl_context = None + +try: + config2 = configparser.ConfigParser() + config2.read(SSL_INI) + + # WAŻNE: has_section check PRZED .get() + if config2.has_section('ssl'): + certificate_path = config2.get('ssl', 'certificate_path') + private_key_path = config2.get('ssl', 'private_key_path') + else: + print(f"[APP] ✗ No [ssl] section in {SSL_INI}", flush=True) + sys.exit(1) + + # Sprawdź czy pliki istnieją + if not os.path.exists(certificate_path): + print(f"[APP] ✗ Certificate not found: {certificate_path}", flush=True) + sys.exit(1) + + if not os.path.exists(private_key_path): + print(f"[APP] ✗ Private key not found: {private_key_path}", flush=True) + sys.exit(1) + + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path) + print(f"[APP] ✓ SSL context loaded", flush=True) + +except Exception as e: + print(f"[APP] ✗ SSL error: {e}", flush=True) + sys.exit(1) + # Statistics Route @app.route('/statistics') @@ -55,11 +114,13 @@ def display_haproxy_stats(): align-items: center; } + .logo { width: 300px; /* Adjust the width as needed */ height: auto; } + .menu-link { text-decoration: none; padding: 10px 20px; @@ -67,6 +128,7 @@ def display_haproxy_stats(): font-weight: bold; } + .menu-link:hover { background-color: #3B444B; color: white; @@ -120,6 +182,7 @@ def display_haproxy_stats(): ''', stats=parsed_stats) + # Logs Route @app.route('/logs') def display_logs(): @@ -127,5 +190,6 @@ def display_logs(): parsed_entries = parse_log_file(log_file_path) return render_template('logs.html', entries=parsed_entries) + if __name__ == '__main__': - app.run(host='::', port=5000, ssl_context=ssl_context, debug=True) + app.run(host='::', port=5000, ssl_context=ssl_context, debug=True) \ No newline at end of file diff --git a/auth/auth_middleware.py b/auth/auth_middleware.py index 22c3b11..8262d29 100644 --- a/auth/auth_middleware.py +++ b/auth/auth_middleware.py @@ -4,7 +4,7 @@ from flask import request, Response import configparser # Docker paths -CONFIG_DIR = '/app/config' +CONFIG_DIR = './config' AUTH_CFG = os.path.join(CONFIG_DIR, 'auth', 'auth.cfg') # Ensure config directory exists diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index 0120ccc..a55174c 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -1,151 +1,174 @@ +import os + +HAPROXY_CFG = '/etc/haproxy/haproxy.cfg' + def is_frontend_exist(frontend_name, frontend_ip, frontend_port): - with open('/etc/haproxy/haproxy.cfg', 'r') as haproxy_cfg: - frontend_found = False - for line in haproxy_cfg: - if line.strip().startswith('frontend'): - _, existing_frontend_name = line.strip().split(' ', 1) - if existing_frontend_name.strip() == frontend_name: - frontend_found = True - else: - frontend_found = False - elif frontend_found and line.strip().startswith('bind'): - _, bind_info = line.strip().split(' ', 1) - existing_ip, existing_port = bind_info.split(':', 1) - if existing_ip.strip() == frontend_ip and existing_port.strip() == frontend_port: - return True + """Check if frontend with given name, IP and port already exists""" + if not os.path.exists(HAPROXY_CFG): + return False + + try: + with open(HAPROXY_CFG, 'r') as haproxy_cfg: + frontend_found = False + for line in haproxy_cfg: + if line.strip().startswith('frontend'): + _, existing_frontend_name = line.strip().split(' ', 1) + if existing_frontend_name.strip() == frontend_name: + frontend_found = True + else: + frontend_found = False + elif frontend_found and line.strip().startswith('bind'): + _, bind_info = line.strip().split(' ', 1) + existing_ip, existing_port = bind_info.split(':', 1) + if existing_ip.strip() == frontend_ip and existing_port.strip() == frontend_port: + return True + except Exception as e: + print(f"[HAPROXY_CONFIG] Error checking frontend: {e}", flush=True) + return False def is_backend_exist(backend_name): - with open('/etc/haproxy/haproxy.cfg', 'r') as haproxy_cfg: - for line in haproxy_cfg: - line = line.strip() - if line.startswith('backend') and not line.startswith('#'): - parts = line.split() - if len(parts) >= 2 and parts[1] == backend_name: - return True + """Check if backend with given name already exists""" + if not os.path.exists(HAPROXY_CFG): + return False + + try: + with open(HAPROXY_CFG, 'r') as haproxy_cfg: + for line in haproxy_cfg: + line = line.strip() + if line.startswith('backend') and not line.startswith('#'): + parts = line.split() + if len(parts) >= 2 and parts[1] == backend_name: + return True + except Exception as e: + print(f"[HAPROXY_CONFIG] Error checking backend: {e}", flush=True) + return False -def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name, backend_servers, health_check, health_check_tcp, health_check_link, sticky_session, add_header, header_name, header_value, sticky_session_type, is_acl, acl_name, acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos, ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name, allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells): - - if is_backend_exist(backend_name): - return f"Backend {backend_name} already exists. Cannot add duplicate." - - with open('/etc/haproxy/haproxy.cfg', 'a') as haproxy_cfg: - haproxy_cfg.write(f"\nfrontend {frontend_name}\n") - if is_frontend_exist(frontend_name, frontend_ip, frontend_port): - return "Frontend or Port already exists. Cannot add duplicate." - haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}") - if use_ssl: - haproxy_cfg.write(f" ssl crt {ssl_cert_path}\n") - if https_redirect: - haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}") - haproxy_cfg.write("\n") - if forward_for: - haproxy_cfg.write(f" option forwardfor\n") - haproxy_cfg.write(f" mode {protocol}\n") - haproxy_cfg.write(f" balance {lb_method}\n") - if is_dos: - haproxy_cfg.write(f" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n") - haproxy_cfg.write(f" http-request track-sc0 src\n") - haproxy_cfg.write(f" acl abuse sc_http_req_rate(0) gt {limit_requests}\n") - haproxy_cfg.write(f" http-request silent-drop if abuse\n") - if sql_injection_check: - haproxy_cfg.write(f" acl is_sql_injection urlp_reg -i (union|select|insert|update|delete|drop|@@|1=1|`1)\n") - haproxy_cfg.write(f" acl is_long_uri path_len gt 400\n") - haproxy_cfg.write(f" acl semicolon_path path_reg -i ^.*;.*\n") - haproxy_cfg.write(r" acl is_sql_injection2 urlp_reg -i (;|substring|extract|union\s+all|order\s+by)\s+(\d+|--\+)" + "\n") - haproxy_cfg.write(f" http-request deny if is_sql_injection or is_long_uri or semicolon_path or is_sql_injection2\n") - if is_xss: - haproxy_cfg.write(f" acl is_xss_attack urlp_reg -i (<|>|script|alert|onerror|onload|javascript)\n") - haproxy_cfg.write(f" acl is_xss_attack_2 urlp_reg -i (<\s*script\s*|javascript:|<\s*img\s*src\s*=|<\s*a\s*href\s*=|<\s*iframe\s*src\s*=|\bon\w+\s*=|<\s*input\s*[^>]*\s*value\s*=|<\s*form\s*action\s*=|<\s*svg\s*on\w+\s*=)\n") - haproxy_cfg.write(f" acl is_xss_attack_hdr hdr_reg(Cookie|Referer|User-Agent) -i (<|>|script|alert|onerror|onload|javascript)\n") - haproxy_cfg.write(" acl is_xss_cookie hdr_beg(Cookie) -i \"= 3: # Ensure we have name, ip and port - backend_server_name = backend_server[0] or f"server{i}" - backend_server_ip = backend_server[1] - backend_server_port = backend_server[2] - backend_server_maxconn = backend_server[3] if len(backend_server) > 3 else None - - line = f" server {backend_server_name} {backend_server_ip}:{backend_server_port} check" - if sticky_session and sticky_session_type == 'cookie': - line += f" cookie {backend_server_name}" - if backend_server_maxconn: - line += f" maxconn {backend_server_maxconn}" - haproxy_cfg.write(line + "\n") - - return "Frontend and Backend added successfully." - def count_frontends_and_backends(): + """Count frontends, backends, ACLs and layer types""" + if not os.path.exists(HAPROXY_CFG): + return 0, 0, 0, 0, 0 + frontend_count = 0 backend_count = 0 acl_count = 0 layer7_count = 0 layer4_count = 0 - with open('/etc/haproxy/haproxy.cfg', 'r') as haproxy_cfg: - lines = haproxy_cfg.readlines() + try: + with open(HAPROXY_CFG, 'r') as haproxy_cfg: + content = haproxy_cfg.read() + lines = content.split('\n') - for line in lines: - line = line.strip() - - if line.startswith('frontend '): - frontend_count += 1 - if line.startswith('acl '): - acl_count += 1 - if line.startswith('mode http'): - layer7_count += 1 - if line.startswith('mode tcp'): - layer4_count += 1 - elif line.startswith('backend '): - backend_count += 1 + for line in lines: + line_stripped = line.strip() + if line_stripped.startswith('frontend'): + frontend_count += 1 + if 'mode http' in content: + layer7_count += 1 + elif 'mode tcp' in content: + layer4_count += 1 + elif line_stripped.startswith('backend'): + backend_count += 1 + elif line_stripped.startswith('acl'): + acl_count += 1 + except Exception as e: + print(f"[HAPROXY_CONFIG] Error counting: {e}", flush=True) return frontend_count, backend_count, acl_count, layer7_count, layer4_count + +def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name, + backend_servers, health_check, health_check_tcp, health_check_link, sticky_session, + add_header, header_name, header_value, sticky_session_type, is_acl, acl_name, + acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos, + ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name, + allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, + add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells): + + # Ensure directory exists + os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True) + + if is_backend_exist(backend_name): + return f"Backend {backend_name} already exists. Cannot add duplicate." + + try: + with open(HAPROXY_CFG, 'a') as haproxy_cfg: + haproxy_cfg.write(f"\nfrontend {frontend_name}\n") + + if is_frontend_exist(frontend_name, frontend_ip, frontend_port): + return "Frontend or Port already exists. Cannot add duplicate." + + haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}") + + if use_ssl: + haproxy_cfg.write(f" ssl crt {ssl_cert_path}") + haproxy_cfg.write("\n") + + if https_redirect: + haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n") + + if forward_for: + haproxy_cfg.write(f" option forwardfor\n") + + haproxy_cfg.write(f" mode {protocol}\n") + haproxy_cfg.write(f" balance {lb_method}\n") + + # Add protection rules + if is_dos: + haproxy_cfg.write(f" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n") + haproxy_cfg.write(f" http-request track-sc0 src\n") + haproxy_cfg.write(f" acl abuse sc_http_req_rate(0) gt {limit_requests}\n") + haproxy_cfg.write(f" http-request silent-drop if abuse\n") + + if sql_injection_check: + # POPRAWNE escape sequence'i - podwójny backslash dla haproxy + haproxy_cfg.write(" acl is_sql_injection urlp_reg -i (union|select|insert|update|delete|drop|@@|1=1|`1)\n") + haproxy_cfg.write(" acl is_long_uri path_len gt 400\n") + haproxy_cfg.write(" acl semicolon_path path_reg -i ^.*;.*\n") + haproxy_cfg.write(" acl is_sql_injection2 urlp_reg -i (;|substring|extract|union\\s+all|order\\s+by)\\s+(\\d+|--\\+)\n") + haproxy_cfg.write(f" http-request deny if is_sql_injection or is_long_uri or semicolon_path or is_sql_injection2\n") + + if is_xss: + haproxy_cfg.write(" acl is_xss_attack urlp_reg -i (<|>|script|alert|onerror|onload|javascript)\n") + haproxy_cfg.write(" acl is_xss_attack_2 urlp_reg -i (<\\s*script\\s*|javascript:|<\\s*img\\s*src\\s*=|<\\s*a\\s*href\\s*=|<\\s*iframe\\s*src\\s*=|\\bon\\w+\\s*=|<\\s*input\\s*[^>]*\\s*value\\s*=|<\\s*form\\s*action\\s*=|<\\s*svg\\s*on\\w+\\s*=)\n") + haproxy_cfg.write(" acl is_xss_attack_hdr hdr_reg(Cookie|Referer|User-Agent) -i (<|>|script|alert|onerror|onload|javascript)\n") + haproxy_cfg.write(f" http-request deny if is_xss_attack or is_xss_attack_2 or is_xss_attack_hdr\n") + + if is_webshells: + haproxy_cfg.write(" acl blocked_webshell path_reg -i /(cmd|shell|backdoor|webshell|phpspy|c99|kacak|b374k|log4j|log4shell|wsos|madspot|malicious|evil).*\\.php.*\n") + haproxy_cfg.write(f" http-request deny if blocked_webshell\n") + + haproxy_cfg.write(f" default_backend {backend_name}\n") + + # Backend section + haproxy_cfg.write(f"\nbackend {backend_name}\n") + haproxy_cfg.write(f" balance {lb_method}\n") + + if sticky_session: + if sticky_session_type == "cookie": + haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n") + elif sticky_session_type == "source": + haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n") + haproxy_cfg.write(f" stick on src\n") + + if health_check and protocol == 'http': + haproxy_cfg.write(f" option httpchk GET {health_check_link}\n") + elif health_check_tcp and protocol == 'tcp': + haproxy_cfg.write(f" option tcp-check\n") + + if add_header: + haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n") + + # Add backend servers + for server_name, server_ip, server_port, maxconn in backend_servers: + maxconn_str = f" maxconn {maxconn}" if maxconn else "" + if health_check and protocol == 'http': + haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n") + else: + haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n") + + return "Configuration updated successfully!" + except Exception as e: + print(f"[HAPROXY_CONFIG] Error updating config: {e}", flush=True) + return f"Error: {e}"