From b305368690d8f70135002a0189566011ee848120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 08:30:30 +0100 Subject: [PATCH 01/33] new options --- routes/edit_routes.py | 2 +- routes/main_routes.py | 169 +++++++---- static/js/form.js | 191 ++++++++++++ static/js/index.js | 249 ++++++++------- templates/index.html | 649 +++++++++++++++++++++++++++------------- utils/haproxy_config.py | 51 +++- utils/stats_utils.py | 2 +- 7 files changed, 943 insertions(+), 370 deletions(-) create mode 100644 static/js/form.js diff --git a/routes/edit_routes.py b/routes/edit_routes.py index b05024d..bcd385b 100644 --- a/routes/edit_routes.py +++ b/routes/edit_routes.py @@ -32,7 +32,7 @@ def edit_haproxy_config(): if result.returncode == 0: if not out: - out = "Configuration file is valid ✅" + out = "Configuration file is valid" level = "success" if "Warning" in out or "Warnings" in out: level = "warning" diff --git a/routes/main_routes.py b/routes/main_routes.py index 95a5436..017ac3a 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -1,11 +1,9 @@ from flask import Blueprint, render_template, request -from auth.auth_middleware import requires_auth # Updated import +from auth.auth_middleware import requires_auth from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends main_bp = Blueprint('main', __name__) - - @main_bp.route('/', methods=['GET', 'POST']) @requires_auth def index(): @@ -16,89 +14,156 @@ def index(): lb_method = request.form['lb_method'] protocol = request.form['protocol'] backend_name = request.form['backend_name'] + + # Header options add_header = 'add_header' in request.form header_name = request.form.get('header_name', '') if add_header else '' header_value = request.form.get('header_value', '') if add_header else '' - - # Get all backend servers data + # Server header removal + del_server_header = 'del_server_header' in request.form + + # Backend servers backend_server_names = request.form.getlist('backend_server_names[]') backend_server_ips = request.form.getlist('backend_server_ips[]') backend_server_ports = request.form.getlist('backend_server_ports[]') backend_server_maxconns = request.form.getlist('backend_server_maxconns[]') - + + # ACL is_acl = 'add_acl' in request.form - acl_name = request.form['acl'] if 'acl' in request.form else '' - acl_action = request.form['acl_action'] if 'acl_action' in request.form else '' - acl_backend_name = request.form['backend_name_acl'] if 'backend_name_acl' in request.form else '' + acl_name = request.form.get('acl', '') + acl_action = request.form.get('acl_action', '') + acl_backend_name = request.form.get('backend_name_acl', '') + + # SSL use_ssl = 'ssl_checkbox' in request.form - ssl_cert_path = request.form['ssl_cert_path'] + ssl_cert_path = request.form.get('ssl_cert_path', '/etc/haproxy/certs/haproxy.pem') https_redirect = 'ssl_redirect_checkbox' in request.form - is_dos = 'add_dos' in request.form if 'add_dos' in request.form else '' - ban_duration = request.form["ban_duration"] - limit_requests = request.form["limit_requests"] + + # DOS Protection + is_dos = 'add_dos' in request.form + ban_duration = request.form.get('ban_duration', '30m') + limit_requests = request.form.get('limit_requests', '100') + + # Forward For forward_for = 'forward_for_check' in request.form - + + # Forbidden paths is_forbidden_path = 'add_acl_path' in request.form - forbidden_name = request.form["forbidden_name"] - allowed_ip = request.form["allowed_ip"] - forbidden_path = request.form["forbidden_path"] - - sql_injection_check = 'sql_injection_check' in request.form if 'sql_injection_check' in request.form else '' - is_xss = 'xss_check' in request.form if 'xss_check' in request.form else '' - is_remote_upload = 'remote_uploads_check' in request.form if 'remote_uploads_check' in request.form else '' - + forbidden_name = request.form.get('forbidden_name', '') + allowed_ip = request.form.get('allowed_ip', '') + forbidden_path = request.form.get('forbidden_path', '') + + # SQL Injection + sql_injection_check = 'sql_injection_check' in request.form + + # XSS + is_xss = 'xss_check' in request.form + + # Remote uploads + is_remote_upload = 'remote_uploads_check' in request.form + + # Path-based redirects add_path_based = 'add_path_based' in request.form - redirect_domain_name = request.form["redirect_domain_name"] - root_redirect = request.form["root_redirect"] - redirect_to = request.form["redirect_to"] - is_webshells = 'webshells_check' in request.form if 'webshells_check' in request.form else '' - - # Combine backend server info into a list of tuples (name, ip, port, maxconns) + redirect_domain_name = request.form.get('redirect_domain_name', '') + root_redirect = request.form.get('root_redirect', '') + redirect_to = request.form.get('redirect_to', '') + + # Webshells + is_webshells = 'webshells_check' in request.form + + # Build backend_servers list backend_servers = [] for i in range(len(backend_server_ips)): name = backend_server_names[i] if i < len(backend_server_names) else f"server{i+1}" ip = backend_server_ips[i] if i < len(backend_server_ips) else '' port = backend_server_ports[i] if i < len(backend_server_ports) else '' maxconn = backend_server_maxconns[i] if i < len(backend_server_maxconns) else None - - if ip and port: # Only add if we have IP and port + + if ip and port: backend_servers.append((name, ip, port, maxconn)) - # Check if frontend or port already exists + # Validate frontend existence 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 + return render_template('index.html', + message="Frontend or Port already exists. Cannot add duplicate.", + message_type="danger") + + # Health checks health_check = False health_check_link = "" if protocol == 'http': health_check = 'health_check' in request.form if health_check: - health_check_link = request.form['health_check_link'] - + health_check_link = request.form.get('health_check_link', '/') + health_check_tcp = False if protocol == 'tcp': health_check_tcp = 'health_check2' in request.form - - # Get sticky session related fields + + # Sticky session sticky_session = False sticky_session_type = "" if 'sticky_session' in request.form: sticky_session = True - sticky_session_type = request.form['sticky_session_type'] - - # Update the HAProxy config file + sticky_session_type = request.form.get('sticky_session_type', 'cookie') + + # Call update_haproxy_config with all parameters message = update_haproxy_config( - frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name, - backend_servers, health_check, health_check_tcp, health_check_link, sticky_session, - add_header, header_name, header_value, sticky_session_type, is_acl, acl_name, - acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos, - ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name, - allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, - add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells + frontend_name=frontend_name, + frontend_ip=frontend_ip, + frontend_port=frontend_port, + lb_method=lb_method, + protocol=protocol, + backend_name=backend_name, + backend_servers=backend_servers, + health_check=health_check, + health_check_tcp=health_check_tcp, + health_check_link=health_check_link, + sticky_session=sticky_session, + add_header=add_header, + header_name=header_name, + header_value=header_value, + sticky_session_type=sticky_session_type, + is_acl=is_acl, + acl_name=acl_name, + acl_action=acl_action, + acl_backend_name=acl_backend_name, + use_ssl=use_ssl, + ssl_cert_path=ssl_cert_path, + https_redirect=https_redirect, + is_dos=is_dos, + ban_duration=ban_duration, + limit_requests=limit_requests, + forward_for=forward_for, + is_forbidden_path=is_forbidden_path, + forbidden_name=forbidden_name, + allowed_ip=allowed_ip, + forbidden_path=forbidden_path, + sql_injection_check=sql_injection_check, + is_xss=is_xss, + is_remote_upload=is_remote_upload, + add_path_based=add_path_based, + redirect_domain_name=redirect_domain_name, + root_redirect=root_redirect, + redirect_to=redirect_to, + is_webshells=is_webshells, + del_server_header=del_server_header ) - return render_template('index.html', message=message) - - return render_template('index.html') - + + # Determine message type + message_type = "success" if "successfully" in message else "danger" + + 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) diff --git a/static/js/form.js b/static/js/form.js new file mode 100644 index 0000000..e5e6443 --- /dev/null +++ b/static/js/form.js @@ -0,0 +1,191 @@ +(() => { + 'use strict'; + + // Toggle health check fields based on protocol + 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') { + document.getElementById('health_check')?.parentElement.parentElement.style.display = 'block'; + tcpHealthCheck.style.display = 'none'; + } else { + document.getElementById('health_check')?.parentElement.parentElement.style.display = 'none'; + tcpHealthCheck.style.display = 'flex'; + } + }; + + protocolSelect?.addEventListener('change', onProtocolChange); + + // Toggle 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); + }); + + // Toggle 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); + }); + + // Toggle 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)); + }); + + // Helper functions + const $ = (sel, root = document) => root.querySelector(sel); + const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); + const toggle = (on, el) => el.classList.toggle('d-none', !on); + + // SSL fields + const sslCheckbox = $('#ssl_checkbox'); + const sslFields = $('#ssl_fields'); + sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields)); + + // DOS + const dosCheckbox = $('#add_dos'); + const dosFields = $('#dos_fields'); + dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields)); + + // HTTP only groups + const httpGroups = $$('.http-only, #forbidden_acl_container'); + const httpToggles = [ + $('#sql_injection_check'), + $('#xss_check'), + $('#remote_uploads_check'), + $('#webshells_check'), + $('#forward_for_check'), + $('#add_acl_path'), + $('#add_path_based'), + ]; + + const forbiddenFields = $('#forbidden_fields'); + const pathFields = $('#base_redirect_fields'); + + const onProtocolChangeExtended = () => { + const isHttp = protocolSelect?.value === 'http'; + httpGroups.forEach(el => toggle(isHttp, el)); + + if (!isHttp) { + [forbiddenFields, pathFields].forEach(el => el && toggle(false, el)); + httpToggles.forEach(input => { + if (input) input.checked = false; + }); + } + }; + + protocolSelect?.addEventListener('change', onProtocolChangeExtended); + onProtocolChangeExtended(); + + // ACL + const aclCheckbox = $('#add_acl'); + const aclFields = $('#acl_fields'); + aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields)); + + // toggles that reveal their fields + const bindToggle = (checkboxSel, targetSel) => { + const cb = $(checkboxSel); + const target = $(targetSel); + cb?.addEventListener('change', () => toggle(cb.checked, target)); + if (cb && target) toggle(cb.checked, target); + }; + + bindToggle('#add_path_based', '#base_redirect_fields'); + bindToggle('#add_acl_path', '#forbidden_fields'); + + // LB Method - obsługa trybu no-lb + const lbMethodSelect = $('#lb_method'); + const backendServersContainer = $('#backend_servers_container'); + const addServerBtn = $('#add_backend_btn'); + + const onLbMethodChange = () => { + const isNoLb = lbMethodSelect?.value === 'no-lb'; + + if (isNoLb) { + // Ukryj przycisk dodawania kolejnych serwerów + if (addServerBtn) addServerBtn.classList.add('d-none'); + + // Zostaw tylko pierwszy serwer i usuń pozostałe + const serverRows = $$('.backend-server-row', backendServersContainer); + serverRows.forEach((row, idx) => { + if (idx > 0) row.remove(); + }); + + // Dodaj informację o trybie no-lb jeśli jeszcze nie istnieje + if (!$('.no-lb-info')) { + const info = document.createElement('div'); + info.className = 'alert alert-info alert-sm no-lb-info mt-2'; + info.innerHTML = 'Tryb no-lb: frontend → backend → pojedynczy serwer. Możesz włączyć XSS, DOS, SQL injection protection itp.'; + if (backendServersContainer?.parentElement) { + backendServersContainer.parentElement.appendChild(info); + } + } + } else { + // Pokaż przycisk dodawania serwerów + if (addServerBtn) addServerBtn.classList.remove('d-none'); + + // Usuń informację o no-lb + const info = $('.no-lb-info'); + if (info) info.remove(); + } + }; + + lbMethodSelect?.addEventListener('change', onLbMethodChange); + if (lbMethodSelect) onLbMethodChange(); + + // 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 = ` +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ `; + + 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(); + } + }); +})(); diff --git a/static/js/index.js b/static/js/index.js index cdb311c..4b85071 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,105 +1,150 @@ (() => { - 'use strict'; - - const $ = (sel, root=document) => root.querySelector(sel); - const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); - - // SSL fields - const sslCheckbox = $('#ssl_checkbox'); - const sslFields = $('#ssl_fields'); - - const toggle = (on, el) => el.classList.toggle('d-none', !on); - - sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields)); - - // DOS - const dosCheckbox = $('#add_dos'); - const dosFields = $('#dos_fields'); - dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields)); - - // HTTP only groups - const protocolSelect = $('#protocol'); - const httpGroups = $$('.http-only, #forbidden_acl_container'); - const httpToggles = [ - $('#sql_injection_check'), - $('#xss_check'), - $('#remote_uploads_check'), - $('#webshells_check'), - $('#forward_for_check'), - $('#add_acl_path'), - $('#add_path_based'), - ]; - const forbiddenFields = $('#forbidden_fields'); - const pathFields = $('#base_redirect_fields'); - - const onProtocolChange = () => { - const isHttp = protocolSelect?.value === 'http'; - httpGroups.forEach(el => toggle(isHttp, el)); - if (!isHttp) { - // hide optional groups if protocol != http - [forbiddenFields, pathFields].forEach(el => el && toggle(false, el)); - httpToggles.forEach(input => { if (input) input.checked = false; }); - } - }; - protocolSelect?.addEventListener('change', onProtocolChange); - onProtocolChange(); - - // ACL - const aclCheckbox = $('#add_acl'); - const aclFields = $('#acl_fields'); - aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields)); - - // toggles that reveal their fields - const bindToggle = (checkboxSel, targetSel) => { - const cb = $(checkboxSel); - const target = $(targetSel); - cb?.addEventListener('change', () => toggle(cb.checked, target)); - // initial - if (cb && target) toggle(cb.checked, target); - }; - bindToggle('#add_path_based', '#base_redirect_fields'); - bindToggle('#add_acl_path', '#forbidden_fields'); - - // Backend rows - let serverCount = 1; - const container = $('#backend_servers_container'); - const addBtn = $('#add_backend_btn'); - - const createRow = () => { - serverCount++; - const row = document.createElement('div'); - row.className = 'row g-3 backend-server-row mt-1'; - row.innerHTML = ` -
- - -
-
- - -
-
- - -
-
- -
- - -
-
`; - row.querySelector('button.btn-danger')?.addEventListener('click', () => { - const rows = $$('.backend-server-row'); - if (rows.length > 1) row.remove(); - else alert('Musi istnieć co najmniej jeden backend.'); + 'use strict'; + + const $ = (sel, root=document) => root.querySelector(sel); + const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); + + // SSL fields + const sslCheckbox = $('#ssl_checkbox'); + const sslFields = $('#ssl_fields'); + const toggle = (on, el) => el.classList.toggle('d-none', !on); + + sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields)); + + // DOS + const dosCheckbox = $('#add_dos'); + const dosFields = $('#dos_fields'); + dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields)); + + // HTTP only groups + const protocolSelect = $('#protocol'); + const httpGroups = $$('.http-only, #forbidden_acl_container'); + const httpToggles = [ + $('#sql_injection_check'), + $('#xss_check'), + $('#remote_uploads_check'), + $('#webshells_check'), + $('#forward_for_check'), + $('#add_acl_path'), + $('#add_path_based'), + ]; + + const forbiddenFields = $('#forbidden_fields'); + const pathFields = $('#base_redirect_fields'); + + const onProtocolChange = () => { + const isHttp = protocolSelect?.value === 'http'; + httpGroups.forEach(el => toggle(isHttp, el)); + + if (!isHttp) { + // hide optional groups if protocol != http + [forbiddenFields, pathFields].forEach(el => el && toggle(false, el)); + httpToggles.forEach(input => { + if (input) input.checked = false; + }); + } + }; + + protocolSelect?.addEventListener('change', onProtocolChange); + onProtocolChange(); + + // ACL + const aclCheckbox = $('#add_acl'); + const aclFields = $('#acl_fields'); + aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields)); + + // toggles that reveal their fields + const bindToggle = (checkboxSel, targetSel) => { + const cb = $(checkboxSel); + const target = $(targetSel); + cb?.addEventListener('change', () => toggle(cb.checked, target)); + // initial + if (cb && target) toggle(cb.checked, target); + }; + + bindToggle('#add_path_based', '#base_redirect_fields'); + bindToggle('#add_acl_path', '#forbidden_fields'); + + const lbMethodSelect = $('#lb_method'); + const backendServersContainer = $('#backend_servers_container'); + const addServerBtn = $('#add_backend_btn'); + + const onLbMethodChange = () => { + const isNoLb = lbMethodSelect?.value === 'no-lb'; + + if (isNoLb) { + if (addServerBtn) addServerBtn.classList.add('d-none'); + + const serverRows = $$('.backend-server-row', backendServersContainer); + serverRows.forEach((row, idx) => { + if (idx > 0) row.remove(); + }); + + if (!$('.no-lb-info')) { + const info = document.createElement('div'); + info.className = 'alert alert-info alert-sm no-lb-info mt-2'; + info.innerHTML = 'Tryb no-lb: frontend → backend → pojedynczy serwer. Możesz włączyć XSS, DOS, SQL injection protection itp.'; + if (backendServersContainer?.parentElement) { + backendServersContainer.parentElement.appendChild(info); + } + } + } else { + if (addServerBtn) addServerBtn.classList.remove('d-none'); + + const info = $('.no-lb-info'); + if (info) info.remove(); + } + }; + + lbMethodSelect?.addEventListener('change', onLbMethodChange); + // Wywołaj na starcie + if (lbMethodSelect) onLbMethodChange(); + + // 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 = ` +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ `; + + 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(); + } }); - return row; - }; - addBtn?.addEventListener('click', () => container?.appendChild(createRow())); - - // auto dismiss alerts - setTimeout(() => $$('.alert').forEach(a => { - if (typeof bootstrap !== 'undefined') new bootstrap.Alert(a).close(); - }), 5000); })(); diff --git a/templates/index.html b/templates/index.html index dcba33a..96b0ade 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,220 +1,453 @@ {% extends "base.html" %} + {% set active_page = "index" %} -{% block title %}HAProxy • Index{% endblock %} -{% block breadcrumb %}{% endblock %} + +{% block title %}HAProxy • Configuration{% endblock %} + +{% block breadcrumb %}Configuration{% endblock %} + {% block content %} -
-
-
-
New frontend
- {% if message %} - - {% endif %} -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
-
- - -
-
-
- -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
- -
-
-
-
-
- - -
+
+
+
+
+
{{ frontend_count|default(0) }}
+

Frontends

-
-
- - -
+
+
+
+
+
+
{{ backend_count|default(0) }}
+

Backends

-
-
- - -
+
+
+
+
+
+
{{ acl_count|default(0) }}
+

ACLs

-
-
- - -
+
+
+
+
+
+
L7: {{ layer7_count|default(0) }} / L4: {{ layer4_count|default(0) }}
+

Layers

-
-
- - -
-
-
-
- -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
- -
Backend pool
-
-
- - -
-
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
- -
+
-{% endblock %} -{% block page_js %} + +{% if message %} + +{% endif %} + +
+
+
+
Add New Configuration
+
+
+ + +
Frontend
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
Choose load balancing algorithm or simple single host
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+ + +
Full path to .pem file
+
+
+
+ + +
+
+
+ +
+ + +
Backend
+ +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + +
+
+
+ + +
+
+
+ +
+
+ +
+ + +
Headers & Security
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ + +
+
+
+ + +
+ Adds: http-response del-header Server (security) +
+
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + +
Protection
+ + +
+
+
+ + +
+
+
+ + +
e.g. 30m, 1h, 24h
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ + +
ACL & Routing
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+ +
+ +
+
+
+ + + {% endblock %} diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index a55174c..9b054c6 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -3,7 +3,6 @@ import os HAPROXY_CFG = '/etc/haproxy/haproxy.cfg' def is_frontend_exist(frontend_name, frontend_ip, frontend_port): - """Check if frontend with given name, IP and port already exists""" if not os.path.exists(HAPROXY_CFG): return False @@ -28,7 +27,6 @@ def is_frontend_exist(frontend_name, frontend_ip, frontend_port): return False def is_backend_exist(backend_name): - """Check if backend with given name already exists""" if not os.path.exists(HAPROXY_CFG): return False @@ -45,8 +43,52 @@ def is_backend_exist(backend_name): return False +def update_simple_haproxy_config(frontend_name, frontend_host, use_ssl, ssl_cert_path, + backend_name, backend_ip, backend_port, + forward_for=True, del_server_header=True): + """ + Tworzy prostą konfigurację frontend->backend bez load balancingu + """ + 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: + # Frontend section + haproxy_cfg.write(f"\nfrontend {frontend_name}\n") + + if use_ssl: + haproxy_cfg.write(f" bind :443 ssl crt {ssl_cert_path}\n") + else: + haproxy_cfg.write(f" bind :80\n") + + # Headers + if forward_for: + haproxy_cfg.write(f" http-request set-header X-Forwarded-For %[src]\n") + haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto {'https' if use_ssl else 'http'}\n") + + if del_server_header: + haproxy_cfg.write(f" http-response del-header Server\n") + + # ACL dla hosta + haproxy_cfg.write(f"\n acl host_{backend_name} hdr(host) -i {frontend_host}\n") + haproxy_cfg.write(f" use_backend {backend_name} if host_{backend_name}\n") + + # Backend section + haproxy_cfg.write(f"\nbackend {backend_name}\n") + haproxy_cfg.write(f" server s1 {backend_ip}:{backend_port} check\n") + + return "Configuration updated successfully!" + + except Exception as e: + print(f"[HAPROXY_CONFIG] Error updating simple config: {e}", flush=True) + return f"Error: {e}" + + + 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 @@ -86,7 +128,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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): @@ -114,7 +155,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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") @@ -122,7 +162,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write(f" http-request silent-drop if abuse\n") if sql_injection_check: - # POPRAWNE escape sequence'i - podwójny backslash dla haproxy haproxy_cfg.write(" acl is_sql_injection urlp_reg -i (union|select|insert|update|delete|drop|@@|1=1|`1)\n") haproxy_cfg.write(" acl is_long_uri path_len gt 400\n") haproxy_cfg.write(" acl semicolon_path path_reg -i ^.*;.*\n") diff --git a/utils/stats_utils.py b/utils/stats_utils.py index 4d2ddb6..38c9f47 100644 --- a/utils/stats_utils.py +++ b/utils/stats_utils.py @@ -1,7 +1,7 @@ import requests import csv -HAPROXY_STATS_URL = 'http://127.0.0.1:8484/;csv' +HAPROXY_STATS_URL = 'http://127.0.0.1:8404/;csv' def fetch_haproxy_stats(): try: From f96b426788df7e0cd4756a8b330da69f3b96d9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 08:42:40 +0100 Subject: [PATCH 02/33] new options --- routes/main_routes.py | 10 ++- static/js/form.js | 24 ++++-- utils/haproxy_config.py | 184 +++++++++++++++++++++------------------- utils/stats_utils.py | 2 +- 4 files changed, 126 insertions(+), 94 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index 017ac3a..6c88105 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -23,6 +23,11 @@ def index(): # Server header removal del_server_header = 'del_server_header' in request.form + # Backend SSL redirect + backend_ssl_redirect = 'backend_ssl_redirect' in request.form + ssl_redirect_backend_name = request.form.get('ssl_redirect_backend_name', '').strip() if backend_ssl_redirect else '' + ssl_redirect_port = request.form.get('ssl_redirect_port', '80') + # Backend servers backend_server_names = request.form.getlist('backend_server_names[]') backend_server_ips = request.form.getlist('backend_server_ips[]') @@ -148,7 +153,10 @@ def index(): root_redirect=root_redirect, redirect_to=redirect_to, is_webshells=is_webshells, - del_server_header=del_server_header + 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 ) # Determine message type diff --git a/static/js/form.js b/static/js/form.js index e5e6443..fea9b55 100644 --- a/static/js/form.js +++ b/static/js/form.js @@ -101,6 +101,18 @@ bindToggle('#add_path_based', '#base_redirect_fields'); bindToggle('#add_acl_path', '#forbidden_fields'); + // Backend SSL redirect + const backendSslCheckbox = $('#backend_ssl_redirect'); + const backendSslFields = $('#backend_ssl_fields'); + + backendSslCheckbox?.addEventListener('change', function() { + toggle(this.checked, backendSslFields); + if (this.checked) { + // Show frontend port field + document.getElementById('ssl_redirect_port')?.parentElement.classList.remove('d-none'); + } + }); + // LB Method - obsługa trybu no-lb const lbMethodSelect = $('#lb_method'); const backendServersContainer = $('#backend_servers_container'); @@ -110,29 +122,29 @@ const isNoLb = lbMethodSelect?.value === 'no-lb'; if (isNoLb) { - // Ukryj przycisk dodawania kolejnych serwerów + // Hide add server button if (addServerBtn) addServerBtn.classList.add('d-none'); - // Zostaw tylko pierwszy serwer i usuń pozostałe + // Keep only first server and remove others const serverRows = $$('.backend-server-row', backendServersContainer); serverRows.forEach((row, idx) => { if (idx > 0) row.remove(); }); - // Dodaj informację o trybie no-lb jeśli jeszcze nie istnieje + // 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 = 'Tryb no-lb: frontend → backend → pojedynczy serwer. Możesz włączyć XSS, DOS, SQL injection protection itp.'; + info.innerHTML = 'Mode no-lb: frontend → backend → single server. You can still enable XSS, DOS, SQL injection protection etc.'; if (backendServersContainer?.parentElement) { backendServersContainer.parentElement.appendChild(info); } } } else { - // Pokaż przycisk dodawania serwerów + // Show add server button if (addServerBtn) addServerBtn.classList.remove('d-none'); - // Usuń informację o no-lb + // Remove no-lb info const info = $('.no-lb-info'); if (info) info.remove(); } diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index 9b054c6..3624593 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -5,7 +5,7 @@ HAPROXY_CFG = '/etc/haproxy/haproxy.cfg' def is_frontend_exist(frontend_name, frontend_ip, frontend_port): if not os.path.exists(HAPROXY_CFG): return False - + try: with open(HAPROXY_CFG, 'r') as haproxy_cfg: frontend_found = False @@ -23,13 +23,13 @@ def is_frontend_exist(frontend_name, frontend_ip, 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): if not os.path.exists(HAPROXY_CFG): return False - + try: with open(HAPROXY_CFG, 'r') as haproxy_cfg: for line in haproxy_cfg: @@ -40,69 +40,24 @@ def is_backend_exist(backend_name): return True except Exception as e: print(f"[HAPROXY_CONFIG] Error checking backend: {e}", flush=True) - + return False -def update_simple_haproxy_config(frontend_name, frontend_host, use_ssl, ssl_cert_path, - backend_name, backend_ip, backend_port, - forward_for=True, del_server_header=True): - """ - Tworzy prostą konfigurację frontend->backend bez load balancingu - """ - 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: - # Frontend section - haproxy_cfg.write(f"\nfrontend {frontend_name}\n") - - if use_ssl: - haproxy_cfg.write(f" bind :443 ssl crt {ssl_cert_path}\n") - else: - haproxy_cfg.write(f" bind :80\n") - - # Headers - if forward_for: - haproxy_cfg.write(f" http-request set-header X-Forwarded-For %[src]\n") - haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto {'https' if use_ssl else 'http'}\n") - - if del_server_header: - haproxy_cfg.write(f" http-response del-header Server\n") - - # ACL dla hosta - haproxy_cfg.write(f"\n acl host_{backend_name} hdr(host) -i {frontend_host}\n") - haproxy_cfg.write(f" use_backend {backend_name} if host_{backend_name}\n") - - # Backend section - haproxy_cfg.write(f"\nbackend {backend_name}\n") - haproxy_cfg.write(f" server s1 {backend_ip}:{backend_port} check\n") - - return "Configuration updated successfully!" - - except Exception as e: - print(f"[HAPROXY_CONFIG] Error updating simple config: {e}", flush=True) - return f"Error: {e}" - - - def count_frontends_and_backends(): if not os.path.exists(HAPROXY_CFG): return 0, 0, 0, 0, 0 - + frontend_count = 0 backend_count = 0 acl_count = 0 layer7_count = 0 layer4_count = 0 - + try: with open(HAPROXY_CFG, 'r') as haproxy_cfg: content = haproxy_cfg.read() lines = content.split('\n') - + for line in lines: line_stripped = line.strip() if line_stripped.startswith('frontend'): @@ -117,97 +72,154 @@ def count_frontends_and_backends(): acl_count += 1 except Exception as e: print(f"[HAPROXY_CONFIG] Error counting: {e}", flush=True) - + return frontend_count, backend_count, acl_count, layer7_count, layer4_count -def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name, - backend_servers, health_check, health_check_tcp, health_check_link, sticky_session, - add_header, header_name, header_value, sticky_session_type, is_acl, acl_name, - acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos, - ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name, - allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, - add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells): - +def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, protocol, backend_name, + backend_servers, health_check, health_check_tcp, health_check_link, sticky_session, + add_header, header_name, header_value, sticky_session_type, is_acl, acl_name, + acl_action, acl_backend_name, use_ssl, ssl_cert_path, https_redirect, is_dos, + ban_duration, limit_requests, forward_for, is_forbidden_path, forbidden_name, + allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, + add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells, + del_server_header=False, backend_ssl_redirect=False, ssl_redirect_backend_name='', + ssl_redirect_port='80'): + 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." - + + # Tryb no-lb - prosty frontend→backend z pojedynczym serwerem + is_no_lb = lb_method == 'no-lb' + if is_no_lb and len(backend_servers) > 1: + backend_servers = backend_servers[:1] # Tylko pierwszy serwer + 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." - + + # Bind line 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") - + + # HTTPS redirect 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") - + + # Ustaw mode - zawsze dla no-lb (nie ma balance) haproxy_cfg.write(f" mode {protocol}\n") - haproxy_cfg.write(f" balance {lb_method}\n") - + + # W trybie no-lb używamy prostych nagłówków HTTP-request + if is_no_lb: + 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") + + # Opcja ukrycia nagłówka Server + if del_server_header: + haproxy_cfg.write(f" http-response del-header Server\n") + else: + # Standardowy tryb z option forwardfor i balance + haproxy_cfg.write(f" balance {lb_method}\n") + + if forward_for: + haproxy_cfg.write(f" option forwardfor\n") + + if del_server_header: + haproxy_cfg.write(f" http-response del-header Server\n") + + # DOS protection - działa w obu trybach 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") - + + # SQL Injection protection - działa w obu trybach if sql_injection_check: 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") - + + # XSS protection - działa w obu trybach 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") - + + # Webshells protection - działa w obu trybach 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") - + + # Default backend 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: + + # Balance tylko dla standardowego trybu + if not is_no_lb: + haproxy_cfg.write(f" balance {lb_method}\n") + + # Sticky sessions - tylko dla standardowego trybu + 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") - + + # Health checks - działa w obu trybach 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") - + + # Custom headers - działa w obu trybach 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!" + + # ========== REDIRECT FRONTEND (HTTP -> HTTPS) ========== + if backend_ssl_redirect and ssl_redirect_backend_name: + # Sprawdź czy taki backend już istnieje + if is_backend_exist(ssl_redirect_backend_name): + return f"Redirect backend {ssl_redirect_backend_name} already exists. Cannot add duplicate." + + haproxy_cfg.write(f"\nfrontend redirect_https\n") + haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n") + haproxy_cfg.write(f" mode http\n") + haproxy_cfg.write(f" default_backend {ssl_redirect_backend_name}\n") + + # Redirect backend + haproxy_cfg.write(f"\nbackend {ssl_redirect_backend_name}\n") + haproxy_cfg.write(f" mode http\n") + haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n") + + return "Configuration updated successfully!" + except Exception as e: print(f"[HAPROXY_CONFIG] Error updating config: {e}", flush=True) return f"Error: {e}" diff --git a/utils/stats_utils.py b/utils/stats_utils.py index 38c9f47..503baf7 100644 --- a/utils/stats_utils.py +++ b/utils/stats_utils.py @@ -1,7 +1,7 @@ import requests import csv -HAPROXY_STATS_URL = 'http://127.0.0.1:8404/;csv' +HAPROXY_STATS_URL = 'http://127.0.0.1:8404/stats;csv' def fetch_haproxy_stats(): try: From 80a0d22d4e7cb8ca461f93101f5770fb7ccbdb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 08:53:47 +0100 Subject: [PATCH 03/33] new options --- spawn.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spawn.sh b/spawn.sh index 7696804..3f928ac 100644 --- a/spawn.sh +++ b/spawn.sh @@ -1,3 +1,4 @@ #!/bin/bash docker compose down -docker compose up --build +docker compose up --remove-orphans --build --no-deps --force-recreate + From 8683af493f8222da393528faa7d5a9eb50e62c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 09:25:56 +0100 Subject: [PATCH 04/33] new options --- static/js/form.js | 128 +++++++++++---------------------------------- static/js/index.js | 106 ++++++------------------------------- 2 files changed, 46 insertions(+), 188 deletions(-) diff --git a/static/js/form.js b/static/js/form.js index fea9b55..f2261fb 100644 --- a/static/js/form.js +++ b/static/js/form.js @@ -1,136 +1,69 @@ (() => { 'use strict'; - - // Toggle health check fields based on protocol + + // 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') { - document.getElementById('health_check')?.parentElement.parentElement.style.display = 'block'; - tcpHealthCheck.style.display = 'none'; + const healthCheckParent = document.getElementById('health_check')?.parentElement.parentElement; + if (healthCheckParent) healthCheckParent.style.display = 'block'; + if (tcpHealthCheck) tcpHealthCheck.style.display = 'none'; } else { - document.getElementById('health_check')?.parentElement.parentElement.style.display = 'none'; - tcpHealthCheck.style.display = 'flex'; + const healthCheckParent = document.getElementById('health_check')?.parentElement.parentElement; + if (healthCheckParent) healthCheckParent.style.display = 'none'; + if (tcpHealthCheck) tcpHealthCheck.style.display = 'flex'; } }; protocolSelect?.addEventListener('change', onProtocolChange); - // Toggle sticky session fields + // ===== 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); + stickyFields?.classList.toggle('d-none', !this.checked); }); - // Toggle health check link field + // ===== 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); }); - // Toggle header fields + // ===== 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)); }); - - // Helper functions - const $ = (sel, root = document) => root.querySelector(sel); - const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); - const toggle = (on, el) => el.classList.toggle('d-none', !on); - - // SSL fields - const sslCheckbox = $('#ssl_checkbox'); - const sslFields = $('#ssl_fields'); - sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields)); - - // DOS - const dosCheckbox = $('#add_dos'); - const dosFields = $('#dos_fields'); - dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields)); - - // HTTP only groups - const httpGroups = $$('.http-only, #forbidden_acl_container'); - const httpToggles = [ - $('#sql_injection_check'), - $('#xss_check'), - $('#remote_uploads_check'), - $('#webshells_check'), - $('#forward_for_check'), - $('#add_acl_path'), - $('#add_path_based'), - ]; - - const forbiddenFields = $('#forbidden_fields'); - const pathFields = $('#base_redirect_fields'); - - const onProtocolChangeExtended = () => { - const isHttp = protocolSelect?.value === 'http'; - httpGroups.forEach(el => toggle(isHttp, el)); - - if (!isHttp) { - [forbiddenFields, pathFields].forEach(el => el && toggle(false, el)); - httpToggles.forEach(input => { - if (input) input.checked = false; - }); - } - }; - - protocolSelect?.addEventListener('change', onProtocolChangeExtended); - onProtocolChangeExtended(); - - // ACL - const aclCheckbox = $('#add_acl'); - const aclFields = $('#acl_fields'); - aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields)); - - // toggles that reveal their fields - const bindToggle = (checkboxSel, targetSel) => { - const cb = $(checkboxSel); - const target = $(targetSel); - cb?.addEventListener('change', () => toggle(cb.checked, target)); - if (cb && target) toggle(cb.checked, target); - }; - - bindToggle('#add_path_based', '#base_redirect_fields'); - bindToggle('#add_acl_path', '#forbidden_fields'); - - // Backend SSL redirect - const backendSslCheckbox = $('#backend_ssl_redirect'); - const backendSslFields = $('#backend_ssl_fields'); - backendSslCheckbox?.addEventListener('change', function() { - toggle(this.checked, backendSslFields); - if (this.checked) { - // Show frontend port field - document.getElementById('ssl_redirect_port')?.parentElement.classList.remove('d-none'); - } - }); - - // LB Method - obsługa trybu no-lb + // ===== 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'); @@ -143,21 +76,21 @@ } 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 rows + + // ===== 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'); @@ -181,23 +114,24 @@
`; - + 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(); } }); + })(); diff --git a/static/js/index.js b/static/js/index.js index 4b85071..075bcbf 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,22 +1,22 @@ (() => { 'use strict'; - const $ = (sel, root=document) => root.querySelector(sel); - const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); + // ===== HELPER FUNCTIONS ===== + const $ = (sel, root = document) => root.querySelector(sel); + const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); + const toggle = (on, el) => el && el.classList.toggle('d-none', !on); - // SSL fields + // ===== SSL FIELDS ===== const sslCheckbox = $('#ssl_checkbox'); const sslFields = $('#ssl_fields'); - const toggle = (on, el) => el.classList.toggle('d-none', !on); - sslCheckbox?.addEventListener('change', () => toggle(sslCheckbox.checked, sslFields)); - // DOS + // ===== DOS PROTECTION ===== const dosCheckbox = $('#add_dos'); const dosFields = $('#dos_fields'); dosCheckbox?.addEventListener('change', () => toggle(dosCheckbox.checked, dosFields)); - // HTTP only groups + // ===== PROTOCOL CHANGE (HTTP/TCP) ===== const protocolSelect = $('#protocol'); const httpGroups = $$('.http-only, #forbidden_acl_container'); const httpToggles = [ @@ -37,8 +37,7 @@ httpGroups.forEach(el => toggle(isHttp, el)); if (!isHttp) { - // hide optional groups if protocol != http - [forbiddenFields, pathFields].forEach(el => el && toggle(false, el)); + [forbiddenFields, pathFields].forEach(el => toggle(false, el)); httpToggles.forEach(input => { if (input) input.checked = false; }); @@ -48,103 +47,28 @@ protocolSelect?.addEventListener('change', onProtocolChange); onProtocolChange(); - // ACL + // ===== ACL FIELDS ===== const aclCheckbox = $('#add_acl'); const aclFields = $('#acl_fields'); aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields)); - // toggles that reveal their fields + // ===== GENERIC TOGGLE BINDER ===== const bindToggle = (checkboxSel, targetSel) => { const cb = $(checkboxSel); const target = $(targetSel); cb?.addEventListener('change', () => toggle(cb.checked, target)); - // initial if (cb && target) toggle(cb.checked, target); }; bindToggle('#add_path_based', '#base_redirect_fields'); bindToggle('#add_acl_path', '#forbidden_fields'); - const lbMethodSelect = $('#lb_method'); - const backendServersContainer = $('#backend_servers_container'); - const addServerBtn = $('#add_backend_btn'); + // ===== BACKEND SSL REDIRECT ===== + const backendSslCheckbox = $('#backend_ssl_redirect'); + const backendSslFields = $('#backend_ssl_fields'); - const onLbMethodChange = () => { - const isNoLb = lbMethodSelect?.value === 'no-lb'; - - if (isNoLb) { - if (addServerBtn) addServerBtn.classList.add('d-none'); - - const serverRows = $$('.backend-server-row', backendServersContainer); - serverRows.forEach((row, idx) => { - if (idx > 0) row.remove(); - }); - - if (!$('.no-lb-info')) { - const info = document.createElement('div'); - info.className = 'alert alert-info alert-sm no-lb-info mt-2'; - info.innerHTML = 'Tryb no-lb: frontend → backend → pojedynczy serwer. Możesz włączyć XSS, DOS, SQL injection protection itp.'; - if (backendServersContainer?.parentElement) { - backendServersContainer.parentElement.appendChild(info); - } - } - } else { - if (addServerBtn) addServerBtn.classList.remove('d-none'); - - const info = $('.no-lb-info'); - if (info) info.remove(); - } - }; - - lbMethodSelect?.addEventListener('change', onLbMethodChange); - // Wywołaj na starcie - if (lbMethodSelect) onLbMethodChange(); - - // 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 = ` -
- -
-
- -
-
- -
-
- -
-
- -
- `; - - const removeBtn = row.querySelector('.remove-server'); - removeBtn.addEventListener('click', () => row.remove()); - - return row; - }; - - addBtn?.addEventListener('click', () => { - if (container) { - container.appendChild(createRow()); - } + backendSslCheckbox?.addEventListener('change', function() { + toggle(this.checked, backendSslFields); }); - // Remove button for dynamically added rows - container?.addEventListener('click', (e) => { - if (e.target.closest('.remove-server')) { - e.target.closest('.backend-server-row').remove(); - } - }); })(); From e4a3671f9083fb0b096367959a6805fff73e68f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 09:32:23 +0100 Subject: [PATCH 05/33] new options --- routes/main_routes.py | 4 ++- templates/index.html | 13 ++++++++- utils/haproxy_config.py | 58 +++++++++++++++++++++++++++-------------- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index 6c88105..cf5ff35 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -11,6 +11,7 @@ def index(): frontend_name = request.form['frontend_name'] frontend_ip = request.form['frontend_ip'] frontend_port = request.form['frontend_port'] + frontend_hostname = request.form.get('frontend_hostname', '').strip() lb_method = request.form['lb_method'] protocol = request.form['protocol'] backend_name = request.form['backend_name'] @@ -156,7 +157,8 @@ def index(): 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 + ssl_redirect_port=ssl_redirect_port, + frontend_hostname=frontend_hostname ) # Determine message type diff --git a/templates/index.html b/templates/index.html index 96b0ade..4da003d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -101,6 +101,16 @@
+
+
+ + +
Domain name for the ACL rule - traffic will be matched by Host header
+
+
+ +
@@ -116,8 +126,9 @@
+ Upload certs in /ssl/ + value="/app/ssl/haproxy-configurator.pem">
Full path to .pem file
diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index 3624593..0609269 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -75,6 +75,10 @@ def count_frontends_and_backends(): return frontend_count, backend_count, acl_count, layer7_count, layer4_count +def sanitize_name(name): + """Convert hostname/name to valid ACL name (replace special chars with underscores)""" + return name.replace('.', '_').replace('-', '_').replace('/', '_').replace(':', '_') + 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, @@ -83,7 +87,7 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells, del_server_header=False, backend_ssl_redirect=False, ssl_redirect_backend_name='', - ssl_redirect_port='80'): + ssl_redirect_port='80', frontend_hostname=''): os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True) @@ -93,10 +97,11 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, # Tryb no-lb - prosty frontend→backend z pojedynczym serwerem is_no_lb = lb_method == 'no-lb' if is_no_lb and len(backend_servers) > 1: - backend_servers = backend_servers[:1] # Tylko pierwszy serwer + backend_servers = backend_servers[:1] try: with open(HAPROXY_CFG, 'a') as haproxy_cfg: + # ===== PRIMARY FRONTEND ===== haproxy_cfg.write(f"\nfrontend {frontend_name}\n") if is_frontend_exist(frontend_name, frontend_ip, frontend_port): @@ -110,13 +115,19 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write("\n") - # HTTPS redirect + # HTTPS redirect (global) if https_redirect: haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n") - # Ustaw mode - zawsze dla no-lb (nie ma balance) + # Mode haproxy_cfg.write(f" mode {protocol}\n") + # ===== HOSTNAME ACL ===== + 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") + # W trybie no-lb używamy prostych nagłówków HTTP-request if is_no_lb: haproxy_cfg.write(f" http-request set-header X-Forwarded-For %[src]\n") @@ -125,11 +136,9 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, else: haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto http\n") - # Opcja ukrycia nagłówka Server if del_server_header: haproxy_cfg.write(f" http-response del-header Server\n") else: - # Standardowy tryb z option forwardfor i balance haproxy_cfg.write(f" balance {lb_method}\n") if forward_for: @@ -138,14 +147,14 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, if del_server_header: haproxy_cfg.write(f" http-response del-header Server\n") - # DOS protection - działa w obu trybach + # DOS protection 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") - # SQL Injection protection - działa w obu trybach + # SQL Injection protection if sql_injection_check: 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") @@ -153,29 +162,34 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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") - # XSS protection - działa w obu trybach + # XSS protection 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") - # Webshells protection - działa w obu trybach + # Webshells protection 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") - # Default backend - haproxy_cfg.write(f" default_backend {backend_name}\n") + # ===== BACKEND ROUTING ===== + if acl_name_sanitized: + # Jeśli jest hostname, routuj z ACL + haproxy_cfg.write(f" use_backend {backend_name} if {acl_name_sanitized}\n") + else: + # Default backend + haproxy_cfg.write(f" default_backend {backend_name}\n") - # Backend section + # ===== PRIMARY BACKEND ===== haproxy_cfg.write(f"\nbackend {backend_name}\n") # Balance tylko dla standardowego trybu if not is_no_lb: haproxy_cfg.write(f" balance {lb_method}\n") - # Sticky sessions - tylko dla standardowego trybu + # Sticky sessions if sticky_session and not is_no_lb: if sticky_session_type == "cookie": haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n") @@ -183,13 +197,13 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n") haproxy_cfg.write(f" stick on src\n") - # Health checks - działa w obu trybach + # Health checks 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") - # Custom headers - działa w obu trybach + # Custom headers if add_header: haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n") @@ -202,16 +216,22 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, else: haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n") - # ========== REDIRECT FRONTEND (HTTP -> HTTPS) ========== + # ===== REDIRECT FRONTEND (HTTP -> HTTPS) ===== if backend_ssl_redirect and ssl_redirect_backend_name: - # Sprawdź czy taki backend już istnieje if is_backend_exist(ssl_redirect_backend_name): return f"Redirect backend {ssl_redirect_backend_name} already exists. Cannot add duplicate." haproxy_cfg.write(f"\nfrontend redirect_https\n") haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n") haproxy_cfg.write(f" mode http\n") - haproxy_cfg.write(f" default_backend {ssl_redirect_backend_name}\n") + + # ===== HOSTNAME ACL FOR REDIRECT FRONTEND ===== + 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 {ssl_redirect_backend_name} if {acl_name_redirect}\n") + else: + haproxy_cfg.write(f" default_backend {ssl_redirect_backend_name}\n") # Redirect backend haproxy_cfg.write(f"\nbackend {ssl_redirect_backend_name}\n") From 72bf6eb9d186478e2110f30766edbfa3733b33c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 09:39:34 +0100 Subject: [PATCH 06/33] new options --- routes/main_routes.py | 50 ++++++++----- spawn.sh | 1 + static/js/index.js | 40 ++++++----- templates/index.html | 152 ++++++++++++++++++++++------------------ utils/haproxy_config.py | 28 +++++++- 5 files changed, 168 insertions(+), 103 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index cf5ff35..f3e92e5 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -35,15 +35,18 @@ def index(): backend_server_ports = request.form.getlist('backend_server_ports[]') backend_server_maxconns = request.form.getlist('backend_server_maxconns[]') - # ACL - is_acl = 'add_acl' in request.form - acl_name = request.form.get('acl', '') - acl_action = request.form.get('acl_action', '') - acl_backend_name = request.form.get('backend_name_acl', '') + # Custom ACL (NEW) + add_custom_acl = 'add_custom_acl' in request.form + custom_acl_name = request.form.get('custom_acl_name', '').strip() if add_custom_acl else '' + custom_acl_type = request.form.get('custom_acl_type', 'path_beg') if add_custom_acl else '' + custom_acl_value = request.form.get('custom_acl_value', '').strip() if add_custom_acl else '' + custom_acl_action = request.form.get('custom_acl_action', 'route') if add_custom_acl else '' + custom_acl_backend = request.form.get('custom_acl_backend', '').strip() if add_custom_acl else '' + custom_acl_redirect_url = request.form.get('custom_acl_redirect_url', '').strip() if add_custom_acl else '' # SSL use_ssl = 'ssl_checkbox' in request.form - ssl_cert_path = request.form.get('ssl_cert_path', '/etc/haproxy/certs/haproxy.pem') + ssl_cert_path = request.form.get('ssl_cert_path', '/app/ssl/haproxy-configurator.pem') https_redirect = 'ssl_redirect_checkbox' in request.form # DOS Protection @@ -54,12 +57,6 @@ def index(): # Forward For forward_for = 'forward_for_check' in request.form - # Forbidden paths - 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', '') - # SQL Injection sql_injection_check = 'sql_injection_check' in request.form @@ -69,14 +66,20 @@ def index(): # Remote uploads is_remote_upload = 'remote_uploads_check' in request.form - # Path-based redirects + # Webshells + is_webshells = 'webshells_check' in request.form + + # Path-based redirects (legacy) add_path_based = 'add_path_based' in request.form redirect_domain_name = request.form.get('redirect_domain_name', '') root_redirect = request.form.get('root_redirect', '') redirect_to = request.form.get('redirect_to', '') - # Webshells - is_webshells = 'webshells_check' in request.form + # 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 = [] @@ -114,7 +117,13 @@ def index(): sticky_session = True sticky_session_type = request.form.get('sticky_session_type', 'cookie') - # Call update_haproxy_config with all parameters + # Legacy ACL (unused, kept for compatibility) + is_acl = False + acl_name = '' + acl_action = '' + acl_backend_name = '' + + # Call update_haproxy_config message = update_haproxy_config( frontend_name=frontend_name, frontend_ip=frontend_ip, @@ -158,7 +167,14 @@ def index(): backend_ssl_redirect=backend_ssl_redirect, ssl_redirect_backend_name=ssl_redirect_backend_name, ssl_redirect_port=ssl_redirect_port, - frontend_hostname=frontend_hostname + 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 ) # Determine message type diff --git a/spawn.sh b/spawn.sh index 3f928ac..aa416ff 100644 --- a/spawn.sh +++ b/spawn.sh @@ -1,4 +1,5 @@ #!/bin/bash +git pull docker compose down docker compose up --remove-orphans --build --no-deps --force-recreate diff --git a/static/js/index.js b/static/js/index.js index 075bcbf..6098631 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -27,6 +27,7 @@ $('#forward_for_check'), $('#add_acl_path'), $('#add_path_based'), + $('#add_custom_acl'), ]; const forbiddenFields = $('#forbidden_fields'); @@ -47,22 +48,6 @@ protocolSelect?.addEventListener('change', onProtocolChange); onProtocolChange(); - // ===== ACL FIELDS ===== - const aclCheckbox = $('#add_acl'); - const aclFields = $('#acl_fields'); - aclCheckbox?.addEventListener('change', () => toggle(aclCheckbox.checked, aclFields)); - - // ===== GENERIC TOGGLE BINDER ===== - const bindToggle = (checkboxSel, targetSel) => { - const cb = $(checkboxSel); - const target = $(targetSel); - cb?.addEventListener('change', () => toggle(cb.checked, target)); - if (cb && target) toggle(cb.checked, target); - }; - - bindToggle('#add_path_based', '#base_redirect_fields'); - bindToggle('#add_acl_path', '#forbidden_fields'); - // ===== BACKEND SSL REDIRECT ===== const backendSslCheckbox = $('#backend_ssl_redirect'); const backendSslFields = $('#backend_ssl_fields'); @@ -71,4 +56,27 @@ toggle(this.checked, backendSslFields); }); + // ===== CUSTOM ACL (Main Toggle) ===== + const customAclCheckbox = $('#add_custom_acl'); + const customAclFields = $('#custom_acl_fields'); + + customAclCheckbox?.addEventListener('change', function() { + toggle(this.checked, customAclFields); + }); + + // ===== CUSTOM ACL Action Type Toggle ===== + const customAclAction = $('#custom_acl_action'); + const aclBackendSelect = $('#acl_backend_select'); + const aclRedirectSelect = $('#acl_redirect_select'); + + const onCustomAclActionChange = () => { + const action = customAclAction?.value; + toggle(action === 'route', aclBackendSelect); + toggle(action === 'redirect', aclRedirectSelect); + }; + + customAclAction?.addEventListener('change', onCustomAclActionChange); + // Initial state + onCustomAclActionChange(); + })(); diff --git a/templates/index.html b/templates/index.html index 4da003d..5a9ca60 100644 --- a/templates/index.html +++ b/templates/index.html @@ -78,6 +78,15 @@ placeholder="80" min="1" max="65535" required>
+ +
+
+ + +
Domain name for the ACL rule - traffic will be matched by Host header
+
+
@@ -101,16 +110,6 @@
-
-
- - -
Domain name for the ACL rule - traffic will be matched by Host header
-
-
- -
@@ -125,8 +124,9 @@
- - Upload certs in /ssl/ + +
Full path to .pem file
@@ -142,6 +142,29 @@
+ +
+
+
+ + +
Creates additional frontend on port 80 to redirect HTTP to HTTPS backend
+
+
+
+ +
+
+ + +
Name for the redirect backend that will push traffic to HTTPS
+
+
+
@@ -369,79 +392,70 @@
- -
ACL & Routing
+ +
Custom ACL Rules (Advanced)
- -
-
- +
+ + +
+
+ + +
Unique identifier for this rule
-
- + + + + +
-
- -
-
- -
-
-
- - -
+
+ + +
Value to match (path, header name, IP, method)
-
- -
-
- -
-
- -
-
- -
-
-
- - -
+
+ +
-
- + +
+ + +
Backend to send matching traffic to
-
- -
-
- + +
+ + +
URL to redirect matching requests to
diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index 0609269..fa524f7 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -87,7 +87,9 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, allowed_ip, forbidden_path, sql_injection_check, is_xss, is_remote_upload, add_path_based, redirect_domain_name, root_redirect, redirect_to, is_webshells, del_server_header=False, backend_ssl_redirect=False, ssl_redirect_backend_name='', - ssl_redirect_port='80', frontend_hostname=''): + ssl_redirect_port='80', frontend_hostname='', add_custom_acl=False, + custom_acl_name='', custom_acl_type='path_beg', custom_acl_value='', + custom_acl_action='route', custom_acl_backend='', custom_acl_redirect_url=''): os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True) @@ -174,6 +176,30 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write(" acl blocked_webshell path_reg -i /(cmd|shell|backdoor|webshell|phpspy|c99|kacak|b374k|log4j|log4shell|wsos|madspot|malicious|evil).*\\.php.*\n") haproxy_cfg.write(f" http-request deny if blocked_webshell\n") + # ===== CUSTOM ACL RULES ===== + if add_custom_acl and custom_acl_name and custom_acl_value: + # Write ACL rule based on type + if custom_acl_type == 'path_beg': + haproxy_cfg.write(f" acl {custom_acl_name} path_beg {custom_acl_value}\n") + elif custom_acl_type == 'path_end': + haproxy_cfg.write(f" acl {custom_acl_name} path_end {custom_acl_value}\n") + elif custom_acl_type == 'path_sub': + haproxy_cfg.write(f" acl {custom_acl_name} path_sub {custom_acl_value}\n") + elif custom_acl_type == 'hdr': + haproxy_cfg.write(f" acl {custom_acl_name} hdr_sub(host) -i {custom_acl_value}\n") + elif custom_acl_type == 'src': + haproxy_cfg.write(f" acl {custom_acl_name} src {custom_acl_value}\n") + elif custom_acl_type == 'method': + haproxy_cfg.write(f" acl {custom_acl_name} method {custom_acl_value}\n") + + # Apply action based on type + if custom_acl_action == 'deny': + haproxy_cfg.write(f" http-request deny if {custom_acl_name}\n") + elif custom_acl_action == 'redirect' and custom_acl_redirect_url: + haproxy_cfg.write(f" http-request redirect location {custom_acl_redirect_url} if {custom_acl_name}\n") + elif custom_acl_action == 'route' and custom_acl_backend: + haproxy_cfg.write(f" use_backend {custom_acl_backend} if {custom_acl_name}\n") + # ===== BACKEND ROUTING ===== if acl_name_sanitized: # Jeśli jest hostname, routuj z ACL From 7a33291342d41b14fcc5d7bfea6bfaacf0d58938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 09:52:14 +0100 Subject: [PATCH 07/33] new options --- templates/index.html | 6 ++-- utils/stats_utils.py | 72 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/templates/index.html b/templates/index.html index 5a9ca60..6fdd959 100644 --- a/templates/index.html +++ b/templates/index.html @@ -124,12 +124,10 @@
- - + -
Full path to .pem file
+
Full path to .pem file, upload certs in /ssl/
diff --git a/utils/stats_utils.py b/utils/stats_utils.py index 503baf7..fd0f3b8 100644 --- a/utils/stats_utils.py +++ b/utils/stats_utils.py @@ -13,18 +13,62 @@ def fetch_haproxy_stats(): def parse_haproxy_stats(stats_data): data = [] - header_row = stats_data.splitlines()[0].replace('# ', '') - reader = csv.DictReader(stats_data.splitlines(), fieldnames=header_row.split(',')) - next(reader) + + # Skip empty lines and get header + lines = [line for line in stats_data.splitlines() if line.strip()] + if not lines: + return data + + header_row = lines[0].replace('# ', '') + + # Parse CSV + reader = csv.DictReader(lines, fieldnames=header_row.split(',')) + next(reader) # Skip header + for row in reader: - if row['svname'] != 'BACKEND': - data.append({ - 'frontend_name': row['pxname'], - 'server_name': row['svname'], - '4xx_errors': row['hrsp_4xx'], - '5xx_errors': row['hrsp_5xx'], - 'bytes_in_mb': f'{float(row["bin"]) / (1024 * 1024):.2f}', - 'bytes_out_mb': f'{float(row["bout"]) / (1024 * 1024):.2f}', - 'conn_tot': row['conn_tot'], - }) - return data \ No newline at end of file + # Only process servers, skip BACKEND summary rows + if row.get('svname') == 'BACKEND': + continue + + # Strip whitespace from values + row = {k: v.strip() if isinstance(v, str) else v for k, v in row.items()} + + # Safe conversion to int/float + 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 = f'{bin_bytes / (1024 * 1024):.2f}' + except (ValueError, TypeError): + bytes_in_mb = '0.00' + + try: + bout_bytes = float(row.get('bout', 0) or 0) + bytes_out_mb = f'{bout_bytes / (1024 * 1024):.2f}' + except (ValueError, TypeError): + bytes_out_mb = '0.00' + + 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, # ✅ Teraz INT + }) + + return data From acef7eb6104d8bc0e532322471d5bb62c3dbb15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 09:53:30 +0100 Subject: [PATCH 08/33] new options --- utils/stats_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/utils/stats_utils.py b/utils/stats_utils.py index fd0f3b8..24b6067 100644 --- a/utils/stats_utils.py +++ b/utils/stats_utils.py @@ -51,24 +51,24 @@ def parse_haproxy_stats(stats_data): try: bin_bytes = float(row.get('bin', 0) or 0) - bytes_in_mb = f'{bin_bytes / (1024 * 1024):.2f}' + bytes_in_mb = bin_bytes / (1024 * 1024) # ✅ FLOAT, nie string! except (ValueError, TypeError): - bytes_in_mb = '0.00' + bytes_in_mb = 0.0 try: bout_bytes = float(row.get('bout', 0) or 0) - bytes_out_mb = f'{bout_bytes / (1024 * 1024):.2f}' + bytes_out_mb = bout_bytes / (1024 * 1024) # ✅ FLOAT, nie string! except (ValueError, TypeError): - bytes_out_mb = '0.00' + 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, # ✅ Teraz INT + 'bytes_in_mb': bytes_in_mb, # ✅ Float + 'bytes_out_mb': bytes_out_mb, # ✅ Float + 'conn_tot': conn_tot, # ✅ Int }) return data From df701186535182651a80a21d12497d8eafb062a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:18:10 +0100 Subject: [PATCH 09/33] new options --- app.py | 20 ++-- log_parser.py | 232 +++++++++++++++++++++++++++++-------------- static/js/logs.js | 22 ++++ templates/logs.html | 202 ++++++++++++++++++++++++++++--------- utils/stats_utils.py | 18 ++-- 5 files changed, 355 insertions(+), 139 deletions(-) diff --git a/app.py b/app.py index 1986d6f..feac89d 100644 --- a/app.py +++ b/app.py @@ -1,14 +1,15 @@ -from flask import Flask, render_template, render_template_string -import configparser +import os +import sys import ssl + +import configparser +from flask import Flask, render_template, render_template_string from routes.main_routes import main_bp from routes.edit_routes import edit_bp from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats from auth.auth_middleware import setup_auth from log_parser import parse_log_file -import os -import sys from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends BASE_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -73,23 +74,23 @@ try: certificate_path = config2.get('ssl', 'certificate_path') private_key_path = config2.get('ssl', 'private_key_path') else: - print(f"[APP] ✗ No [ssl] section in {SSL_INI}", flush=True) + print(f"[APP] No [ssl] section in {SSL_INI}", flush=True) sys.exit(1) if not os.path.exists(certificate_path): - print(f"[APP] ✗ Certificate not found: {certificate_path}", flush=True) + print(f"[APP] Certificate not found: {certificate_path}", flush=True) sys.exit(1) if not os.path.exists(private_key_path): - print(f"[APP] ✗ Private key not found: {private_key_path}", flush=True) + print(f"[APP] Private key not found: {private_key_path}", flush=True) sys.exit(1) ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path) - print(f"[APP] ✓ SSL context loaded", flush=True) + print(f"[APP] SSL context loaded", flush=True) except Exception as e: - print(f"[APP] ✗ SSL error: {e}", flush=True) + print(f"[APP] SSL error: {e}", flush=True) sys.exit(1) @@ -105,7 +106,6 @@ def display_logs(): parsed_entries = parse_log_file(log_file_path) return render_template('logs.html', entries=parsed_entries) - @app.route('/home') def home(): frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends() diff --git a/log_parser.py b/log_parser.py index 414b506..984ac37 100644 --- a/log_parser.py +++ b/log_parser.py @@ -1,7 +1,11 @@ import re +from collections import defaultdict +from datetime import datetime def parse_log_file(log_file_path): + parsed_entries = [] + xss_patterns = [ r'<\s*script\s*', r'javascript:', @@ -16,88 +20,170 @@ def parse_log_file(log_file_path): r'alert', r'onerror', r'onload', - r'javascript' ] - + sql_patterns = [ - r';', - r'substring', - r'extract', - r'union\s+all', - r'order\s+by', + r'(union|select|insert|update|delete|drop)\s+(from|into|table)', + r';\s*(union|select|insert|update|delete|drop)', + r'substring\s*\(', + r'extract\s*\(', + r'order\s+by\s+\d+', r'--\+', - r'union', - r'select', - r'insert', - r'update', - r'delete', - r'drop', - r'@@', - r'1=1', + r'1\s*=\s*1', + r'@@\w+', r'`1', - r'union', - r'select', - r'insert', - r'update', - r'delete', - r'drop', - r'@@', - r'1=1', - r'`1' + r'\|\|\s*chr\(', ] - + webshells_patterns = [ - r'payload', - r'eval|system|passthru|shell_exec|exec|popen|proc_open|pcntl_exec|cmd|shell|backdoor|webshell|phpspy|c99|kacak|b374k|log4j|log4shell|wsos|madspot|malicious|evil.*\.php.*' + r'eval\s*\(', + r'system\s*\(', + r'passthru\s*\(', + r'shell_exec\s*\(', + r'exec\s*\(', + r'popen\s*\(', + r'proc_open\s*\(', + r'pcntl_exec\s*\(', + r'\.php\?cmd=', + r'\.php\?id=', + r'backdoor|webshell|phpspy|c99|kacak|b374k|wsos|madspot|r57|c100|r57shell', ] - - combined_xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE) - combined_sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE) - combined_webshells_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE) - - with open(log_file_path, 'r') as log_file: - log_lines = log_file.readlines() + + xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE) + sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE) + webshell_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE) + + try: + with open(log_file_path, 'r') as log_file: + log_lines = log_file.readlines() + for line in log_lines: - if " 403 " in line: # Check if the line contains " 403 " indicating a 403 status code - match = re.search(r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"\s*(GET|POST|PUT|DELETE)\s+([^"]+)"', line) - if match: - timestamp = match.group(1) # Extract the date and time - ip_address = match.group(2) - http_method = match.group(3) - requested_url = match.group(4) + if not line.strip(): + continue + + match = re.search( + r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"?\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^"\s]+)"?\s+(\d{3})', + line + ) + + if not match: + continue + + timestamp = match.group(1) + ip_address = match.group(2) + http_method = match.group(3) + requested_url = match.group(4) + status_code = int(match.group(5)) + + threats = [] + threat_level = 'info' + + if xss_pattern.search(line): + threats.append('XSS Attack') + threat_level = 'danger' + + if sql_pattern.search(line): + threats.append('SQL Injection') + threat_level = 'danger' + + if webshell_pattern.search(line): + threats.append('Webshell') + threat_level = 'danger' + + if http_method == 'PUT': + threats.append('Remote Upload') + threat_level = 'warning' + + if 'admin' in requested_url.lower() or 'config' in requested_url.lower(): + if status_code == 403: + threats.append('Unauthorized Access') + threat_level = 'warning' + + status_category = 'info' + if 200 <= status_code < 300: + status_category = 'success' + elif 300 <= status_code < 400: + status_category = 'secondary' + elif 400 <= status_code < 500: + status_category = 'warning' + elif status_code >= 500: + status_category = 'danger' + + parsed_entries.append({ + 'timestamp': timestamp, + 'ip_address': ip_address, + 'http_method': http_method, + 'requested_url': requested_url, + 'status_code': status_code, + 'status_category': status_category, + 'threats': threats if threats else ['None'], + 'threat_level': threat_level if threats else 'info', + 'is_threat': bool(threats), + }) + + except FileNotFoundError: + return [{'error': f'Log file not found: {log_file_path}'}] + except Exception as e: + return [{'error': f'Error parsing log: {str(e)}'}] + + return parsed_entries - if combined_xss_pattern.search(line): - xss_alert = 'Possible XSS Attack Was Identified.' - else: - xss_alert = '' - if combined_sql_pattern.search(line): - sql_alert = 'Possible SQL Injection Attempt Was Made.' - else: - sql_alert = '' - if "PUT" in line: - put_method = 'Possible Remote File Upload Attempt Was Made.' - else: - put_method = '' - if "admin" in line: - illegal_resource = 'Possible Illegal Resource Access Attempt Was Made.' - else: - illegal_resource = '' +def get_log_statistics(parsed_entries): - if combined_webshells_pattern.search(line): - webshell_alert = 'Possible WebShell Attack Attempt Was Made.' - else: - webshell_alert = '' + stats = { + 'total_requests': len(parsed_entries), + 'threat_count': sum(1 for e in parsed_entries if e.get('is_threat')), + 'status_codes': defaultdict(int), + 'http_methods': defaultdict(int), + 'top_ips': defaultdict(int), + 'threat_types': defaultdict(int), + } + + for entry in parsed_entries: + if 'error' in entry: + continue + + stats['status_codes'][entry['status_code']] += 1 + stats['http_methods'][entry['http_method']] += 1 + stats['top_ips'][entry['ip_address']] += 1 + + for threat in entry.get('threats', []): + if threat != 'None': + stats['threat_types'][threat] += 1 + + stats['top_ips'] = sorted( + stats['top_ips'].items(), + key=lambda x: x[1], + reverse=True + )[:5] + + stats['status_codes'] = dict(stats['status_codes']) + stats['http_methods'] = dict(stats['http_methods']) + stats['threat_types'] = dict(stats['threat_types']) + + return stats - parsed_entries.append({ - 'timestamp': timestamp, - 'ip_address': ip_address, - 'http_method': http_method, - 'requested_url': requested_url, - 'xss_alert': xss_alert, - 'sql_alert': sql_alert, - 'put_method': put_method, - 'illegal_resource': illegal_resource, - 'webshell_alert': webshell_alert - }) - return parsed_entries \ No newline at end of file + +def filter_logs(parsed_entries, filters=None): + if not filters: + return parsed_entries + + filtered = parsed_entries + + if 'status_code' in filters and filters['status_code']: + filtered = [e for e in filtered if e.get('status_code') == int(filters['status_code'])] + + if 'threat_level' in filters and filters['threat_level']: + filtered = [e for e in filtered if e.get('threat_level') == filters['threat_level']] + + if 'http_method' in filters and filters['http_method']: + filtered = [e for e in filtered if e.get('http_method') == filters['http_method']] + + if 'ip_address' in filters and filters['ip_address']: + filtered = [e for e in filtered if e.get('ip_address') == filters['ip_address']] + + if 'has_threat' in filters and filters['has_threat']: + filtered = [e for e in filtered if e.get('is_threat')] + + return filtered diff --git a/static/js/logs.js b/static/js/logs.js index e69de29..5fe277c 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -0,0 +1,22 @@ +document.getElementById('filter_status')?.addEventListener('change', filterLogs); +document.getElementById('filter_threat')?.addEventListener('change', filterLogs); +document.getElementById('filter_method')?.addEventListener('change', filterLogs); +document.getElementById('filter_threats_only')?.addEventListener('change', filterLogs); + +function filterLogs() { + const statusFilter = document.getElementById('filter_status')?.value; + const threatFilter = document.getElementById('filter_threat')?.value; + const methodFilter = document.getElementById('filter_method')?.value; + const threatsOnly = document.getElementById('filter_threats_only')?.checked; + + document.querySelectorAll('.log-row').forEach(row => { + let show = true; + + if (statusFilter && row.dataset.status !== statusFilter) show = false; + if (threatFilter && row.dataset.threat !== threatFilter) show = false; + if (methodFilter && row.dataset.method !== methodFilter) show = false; + if (threatsOnly && row.dataset.threatCount === '0') show = false; + + row.style.display = show ? '' : 'none'; + }); +} \ No newline at end of file diff --git a/templates/logs.html b/templates/logs.html index 94f62b7..47a26e0 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -1,50 +1,162 @@ {% extends "base.html" %} -{% set active_page = "" %} -{% block title %}HAProxy • Logs{% endblock %} -{% block breadcrumb %}{% endblock %} + +{% set active_page = "logs" %} + +{% block title %}HAProxy • Access Logs{% endblock %} + +{% block breadcrumb %}Access Logs{% endblock %} + {% block content %} -

Status 403 Forbidden

-{% if entries %} -
- {% for entry in entries %} -
-
-
-
-
Czas: {{ entry['timestamp'] }}
-
IP: {{ entry['ip_address'] }}
-
Metoda: {{ entry['http_method'] }}
-
URL: {{ entry['requested_url'] }}
-
Status: 403
+ +
+
+
HAProxy Access Logs & Security Analysis
+
+
+ + {% if logs %} + +
+
+
+
+
Total Requests
+
{{ logs|length }}
+
+
+
+
+
+
+
Threats Detected
+
+ {{ logs|selectattr('is_threat')|list|length }} +
+
+
+
+
+
+
+
Unique IPs
+
+ {{ logs|map(attribute='ip_address')|unique|list|length }} +
+
+
+
+
+
+
+
Success Rate
+
+ {% set success_count = logs|selectattr('status_code')|selectattr('status_code', 'ge', 200)|selectattr('status_code', 'lt', 300)|list|length %} + {{ ((success_count / logs|length * 100)|round(1)) if logs else 0 }}% +
+
+
+
-
- {% if entry['xss_alert'] %} -

-
{{ entry['xss_alert'] }}
- {% endif %} - {% if entry['sql_alert'] %} -

-
{{ entry['sql_alert'] }}
- {% endif %} - {% if entry['put_method'] %} -

-
{{ entry['put_method'] }}
- {% endif %} - {% if entry['illegal_resource'] %} -

-
{{ entry['illegal_resource'] }}
- {% endif %} - {% if entry['webshell_alert'] %} -

-
{{ entry['webshell_alert'] }}
- {% endif %} + + +
+
+
Filters
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
-
-
-
- {% endfor %} -
-{% else %} -
No data.
-{% endif %} + + +
+ + + + + + + + + + + + + {% for log in logs %} + + + + + + + + + {% endfor %} + +
TimestampIP AddressMethodURLStatusThreats
{{ log.timestamp }} + {{ log.ip_address }} + + {{ log.http_method }} + + {{ log.requested_url }} + + {{ log.status_code }} + + {% if log.is_threat %} + {% for threat in log.threats %} + {{ threat }} + {% endfor %} + {% else %} + + {% endif %} +
+
+ + {% else %} +
+ No log entries found. +
+ {% endif %} + +
+
+ + + {% endblock %} diff --git a/utils/stats_utils.py b/utils/stats_utils.py index 24b6067..bd0a5df 100644 --- a/utils/stats_utils.py +++ b/utils/stats_utils.py @@ -13,8 +13,7 @@ def fetch_haproxy_stats(): def parse_haproxy_stats(stats_data): data = [] - - # Skip empty lines and get header + lines = [line for line in stats_data.splitlines() if line.strip()] if not lines: return data @@ -23,17 +22,14 @@ def parse_haproxy_stats(stats_data): # Parse CSV reader = csv.DictReader(lines, fieldnames=header_row.split(',')) - next(reader) # Skip header + next(reader) for row in reader: - # Only process servers, skip BACKEND summary rows if row.get('svname') == 'BACKEND': continue - # Strip whitespace from values row = {k: v.strip() if isinstance(v, str) else v for k, v in row.items()} - # Safe conversion to int/float try: conn_tot = int(row.get('conn_tot', 0) or 0) except (ValueError, TypeError): @@ -51,13 +47,13 @@ def parse_haproxy_stats(stats_data): try: bin_bytes = float(row.get('bin', 0) or 0) - bytes_in_mb = bin_bytes / (1024 * 1024) # ✅ FLOAT, nie string! + 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) # ✅ FLOAT, nie string! + bytes_out_mb = bout_bytes / (1024 * 1024) except (ValueError, TypeError): bytes_out_mb = 0.0 @@ -66,9 +62,9 @@ def parse_haproxy_stats(stats_data): 'server_name': row.get('svname', 'Unknown'), '4xx_errors': hrsp_4xx, '5xx_errors': hrsp_5xx, - 'bytes_in_mb': bytes_in_mb, # ✅ Float - 'bytes_out_mb': bytes_out_mb, # ✅ Float - 'conn_tot': conn_tot, # ✅ Int + 'bytes_in_mb': bytes_in_mb, + 'bytes_out_mb': bytes_out_mb, + 'conn_tot': conn_tot, }) return data From 014dc76ff6f4d5314f1a4b54b65216fddbb3dcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:22:36 +0100 Subject: [PATCH 10/33] new options --- static/js/logs.js | 106 ++++++++++++++--- templates/logs.html | 271 +++++++++++++++++++++++--------------------- 2 files changed, 230 insertions(+), 147 deletions(-) diff --git a/static/js/logs.js b/static/js/logs.js index 5fe277c..23045ec 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -1,22 +1,92 @@ -document.getElementById('filter_status')?.addEventListener('change', filterLogs); -document.getElementById('filter_threat')?.addEventListener('change', filterLogs); -document.getElementById('filter_method')?.addEventListener('change', filterLogs); -document.getElementById('filter_threats_only')?.addEventListener('change', filterLogs); - -function filterLogs() { - const statusFilter = document.getElementById('filter_status')?.value; - const threatFilter = document.getElementById('filter_threat')?.value; - const methodFilter = document.getElementById('filter_method')?.value; - const threatsOnly = document.getElementById('filter_threats_only')?.checked; +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 resetBtn = document.getElementById('reset_filters'); - document.querySelectorAll('.log-row').forEach(row => { - let show = true; + const logsTable = document.getElementById('logs_table'); + 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; - if (statusFilter && row.dataset.status !== statusFilter) show = false; - if (threatFilter && row.dataset.threat !== threatFilter) show = false; - if (methodFilter && row.dataset.method !== methodFilter) show = false; - if (threatsOnly && row.dataset.threatCount === '0') show = false; + let visibleCount = 0; + let threatCount = 0; + let count2xx = 0, count4xx = 0, count5xx = 0; + const uniqueIps = new Set(); - row.style.display = show ? '' : 'none'; + allRows.forEach(row => { + const ip = row.dataset.ip; + const status = row.dataset.status; + const method = row.dataset.method; + const hasThreat = row.dataset.threats === '1'; + + 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; + } + + 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); + + // Reset button + resetBtn.addEventListener('click', function() { + filterIp.value = ''; + filterStatus.value = ''; + filterMethod.value = ''; + filterThreats.checked = true; + applyFilters(); }); -} \ No newline at end of file + + // Initial stats + applyFilters(); +}); \ No newline at end of file diff --git a/templates/logs.html b/templates/logs.html index 47a26e0..546f4ae 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -2,151 +2,165 @@ {% set active_page = "logs" %} -{% block title %}HAProxy • Access Logs{% endblock %} +{% block title %}HAProxy • Logs{% endblock %} -{% block breadcrumb %}Access Logs{% endblock %} +{% block breadcrumb %}Logs{% endblock %} {% block content %}
-
HAProxy Access Logs & Security Analysis
+
HAProxy Access Logs
+ {% if logs %} - -
-
-
-
-
Total Requests
-
{{ logs|length }}
-
-
-
-
-
-
-
Threats Detected
-
- {{ logs|selectattr('is_threat')|list|length }} -
-
-
-
-
-
-
-
Unique IPs
-
- {{ logs|map(attribute='ip_address')|unique|list|length }} -
-
-
-
-
-
-
-
Success Rate
-
- {% set success_count = logs|selectattr('status_code')|selectattr('status_code', 'ge', 200)|selectattr('status_code', 'lt', 300)|list|length %} - {{ ((success_count / logs|length * 100)|round(1)) if logs else 0 }}% -
-
-
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
- -
-
-
Filters
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
+ +
+
+
+
+
Total
+ {{ logs|length }}
- - -
- - - - - - - - - - - - - {% for log in logs %} - - - - - - - - - {% endfor %} - -
TimestampIP AddressMethodURLStatusThreats
{{ log.timestamp }} - {{ log.ip_address }} - - {{ log.http_method }} - - {{ log.requested_url }} - - {{ log.status_code }} - - {% if log.is_threat %} - {% for threat in log.threats %} - {{ threat }} - {% endfor %} - {% else %} - - {% endif %} -
+
+
+
+
Threats
+ 0 +
+
+
+
+
+
2xx
+ 0 +
+
+
+
+
+
+
4xx
+ 0 +
+
+
+
+
+
+
5xx
+ 0 +
+
+
+
+
+
+
Unique IPs
+ 0 +
+
+
+
+ +
+ +
+ + + + + + + + + + + + + {% for entry in logs %} + + + + + + + + + {% endfor %} + +
TimestampIP AddressHTTP MethodRequested URLStatus CodeAlerts
{{ entry['timestamp'] }} + {{ entry['ip_address'] }} + + {{ entry['http_method'] }} + + {{ entry['requested_url'] }} + + + {{ entry['status_code'] }} + + + {% if entry['xss_alert'] %} + XSS + {% endif %} + {% if entry['sql_alert'] %} + SQL + {% endif %} + {% if entry['put_method'] %} + PUT + {% endif %} + {% if entry['webshell_alert'] %} + Webshell + {% endif %} + {% if entry['illegal_resource'] %} + 403 + {% endif %} +
+
{% else %}
@@ -158,5 +172,4 @@
- {% endblock %} From d01ca3512e4e06e3be00bb1307fafcb43738e7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:27:52 +0100 Subject: [PATCH 11/33] new options --- log_parser.py | 210 ++++++++++++++++---------------------------- templates.tar.gz | Bin 10423 -> 0 bytes templates/logs.html | 3 +- 3 files changed, 79 insertions(+), 134 deletions(-) delete mode 100644 templates.tar.gz diff --git a/log_parser.py b/log_parser.py index 984ac37..330d489 100644 --- a/log_parser.py +++ b/log_parser.py @@ -1,11 +1,13 @@ import re -from collections import defaultdict -from datetime import datetime def parse_log_file(log_file_path): - + """ + Parse HAProxy syslog format and identify security threats. + Format: <134>Nov 3 09:18:35 haproxy[18]: IP:PORT [DATE:TIME] FRONTEND BACKEND STATUS BYTES ... + """ parsed_entries = [] + # Security threat patterns xss_patterns = [ r'<\s*script\s*', r'javascript:', @@ -16,8 +18,7 @@ def parse_log_file(log_file_path): r'<\s*input\s*[^>]*\s*value\s*=?', r'<\s*form\s*action\s*=?', r'<\s*svg\s*on\w+\s*=?', - r'script', - r'alert', + r'alert\s*\(', r'onerror', r'onload', ] @@ -32,7 +33,6 @@ def parse_log_file(log_file_path): r'1\s*=\s*1', r'@@\w+', r'`1', - r'\|\|\s*chr\(', ] webshells_patterns = [ @@ -43,147 +43,93 @@ def parse_log_file(log_file_path): r'exec\s*\(', r'popen\s*\(', r'proc_open\s*\(', - r'pcntl_exec\s*\(', - r'\.php\?cmd=', - r'\.php\?id=', - r'backdoor|webshell|phpspy|c99|kacak|b374k|wsos|madspot|r57|c100|r57shell', + r'backdoor|webshell|phpspy|c99|kacak|b374k|wsos', ] + # Compile patterns xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE) sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE) webshell_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE) try: - with open(log_file_path, 'r') as log_file: + with open(log_file_path, 'r', encoding='utf-8', errors='ignore') as log_file: log_lines = log_file.readlines() for line in log_lines: if not line.strip(): continue - - match = re.search( - r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"?\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^"\s]+)"?\s+(\d{3})', - line - ) - - if not match: + + try: + # Extract syslog header + syslog_match = re.search( + r'<\d+>(\w+\s+\d+\s+\d+:\d+:\d+).*haproxy\[\d+\]:\s+', + line + ) + + if not syslog_match: + continue + + timestamp = syslog_match.group(1) + + # Extract IP:PORT + ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', line) + if not ip_match: + continue + + ip_address = ip_match.group(1) + + # Extract date/time in brackets + datetime_match = re.search(r'\[(\d{2}/\w+/\d{4}:\d{2}:\d{2}:\d{2})', line) + if datetime_match: + timestamp = datetime_match.group(1) + + # Extract frontend and backend + fe_be_match = re.search(r'\]\s+(\S+)\s+(\S+)\s+(\d+/\d+/\d+/\d+/\d+)\s+(\d{3})', line) + if not fe_be_match: + continue + + frontend = fe_be_match.group(1) + backend = fe_be_match.group(2) + status_code = fe_be_match.group(4) + + # Extract HTTP method and URL + http_match = re.search(r'"(\w+)\s+([^\s]+)\s+HTTP', line) + if not http_match: + continue + + http_method = http_match.group(1) + requested_url = http_match.group(2) + + # Detect threats + xss_alert = bool(xss_pattern.search(line)) + sql_alert = bool(sql_pattern.search(line)) + webshell_alert = bool(webshell_pattern.search(line)) + put_method = http_method == 'PUT' + illegal_resource = status_code == '403' + + parsed_entries.append({ + 'timestamp': timestamp, + 'ip_address': ip_address, + 'http_method': http_method, + 'requested_url': requested_url, + 'status_code': status_code, + 'frontend': frontend, + 'backend': backend, + 'xss_alert': xss_alert, + 'sql_alert': sql_alert, + 'put_method': put_method, + 'illegal_resource': illegal_resource, + 'webshell_alert': webshell_alert, + }) + except Exception as e: + print(f"Error parsing line: {e}") continue - - timestamp = match.group(1) - ip_address = match.group(2) - http_method = match.group(3) - requested_url = match.group(4) - status_code = int(match.group(5)) - - threats = [] - threat_level = 'info' - - if xss_pattern.search(line): - threats.append('XSS Attack') - threat_level = 'danger' - - if sql_pattern.search(line): - threats.append('SQL Injection') - threat_level = 'danger' - - if webshell_pattern.search(line): - threats.append('Webshell') - threat_level = 'danger' - - if http_method == 'PUT': - threats.append('Remote Upload') - threat_level = 'warning' - - if 'admin' in requested_url.lower() or 'config' in requested_url.lower(): - if status_code == 403: - threats.append('Unauthorized Access') - threat_level = 'warning' - - status_category = 'info' - if 200 <= status_code < 300: - status_category = 'success' - elif 300 <= status_code < 400: - status_category = 'secondary' - elif 400 <= status_code < 500: - status_category = 'warning' - elif status_code >= 500: - status_category = 'danger' - - parsed_entries.append({ - 'timestamp': timestamp, - 'ip_address': ip_address, - 'http_method': http_method, - 'requested_url': requested_url, - 'status_code': status_code, - 'status_category': status_category, - 'threats': threats if threats else ['None'], - 'threat_level': threat_level if threats else 'info', - 'is_threat': bool(threats), - }) except FileNotFoundError: - return [{'error': f'Log file not found: {log_file_path}'}] + print(f"Log file not found: {log_file_path}") + return [] except Exception as e: - return [{'error': f'Error parsing log: {str(e)}'}] + print(f"Error reading log file: {e}") + return [] return parsed_entries - - -def get_log_statistics(parsed_entries): - - stats = { - 'total_requests': len(parsed_entries), - 'threat_count': sum(1 for e in parsed_entries if e.get('is_threat')), - 'status_codes': defaultdict(int), - 'http_methods': defaultdict(int), - 'top_ips': defaultdict(int), - 'threat_types': defaultdict(int), - } - - for entry in parsed_entries: - if 'error' in entry: - continue - - stats['status_codes'][entry['status_code']] += 1 - stats['http_methods'][entry['http_method']] += 1 - stats['top_ips'][entry['ip_address']] += 1 - - for threat in entry.get('threats', []): - if threat != 'None': - stats['threat_types'][threat] += 1 - - stats['top_ips'] = sorted( - stats['top_ips'].items(), - key=lambda x: x[1], - reverse=True - )[:5] - - stats['status_codes'] = dict(stats['status_codes']) - stats['http_methods'] = dict(stats['http_methods']) - stats['threat_types'] = dict(stats['threat_types']) - - return stats - - -def filter_logs(parsed_entries, filters=None): - if not filters: - return parsed_entries - - filtered = parsed_entries - - if 'status_code' in filters and filters['status_code']: - filtered = [e for e in filtered if e.get('status_code') == int(filters['status_code'])] - - if 'threat_level' in filters and filters['threat_level']: - filtered = [e for e in filtered if e.get('threat_level') == filters['threat_level']] - - if 'http_method' in filters and filters['http_method']: - filtered = [e for e in filtered if e.get('http_method') == filters['http_method']] - - if 'ip_address' in filters and filters['ip_address']: - filtered = [e for e in filtered if e.get('ip_address') == filters['ip_address']] - - if 'has_threat' in filters and filters['has_threat']: - filtered = [e for e in filtered if e.get('is_threat')] - - return filtered diff --git a/templates.tar.gz b/templates.tar.gz deleted file mode 100644 index 5e73581ac33a9056fa6bb1974863a6154407696d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10423 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 diff --git a/templates/logs.html b/templates/logs.html index 546f4ae..8c1b1f2 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -9,12 +9,11 @@ {% block content %}
-
+
HAProxy Access Logs
- {% if logs %}
From 58205be555f3863f5fb1a8eaded84503a80d641c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:32:38 +0100 Subject: [PATCH 12/33] new options --- app.py | 24 +++++++++++++++++++++--- static/js/logs.js | 2 ++ templates/index.html | 10 +++++++++- templates/logs.html | 44 +++++++++++++++++++++++++++++++++++++++----- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index feac89d..3538ed0 100644 --- a/app.py +++ b/app.py @@ -100,11 +100,29 @@ def display_haproxy_stats(): parsed_stats = parse_haproxy_stats(haproxy_stats) return render_template('statistics.html', stats=parsed_stats) + @app.route('/logs') -def display_logs(): +@requires_auth +def display_haproxy_logs(): log_file_path = '/var/log/haproxy.log' - parsed_entries = parse_log_file(log_file_path) - return render_template('logs.html', entries=parsed_entries) + + if not os.path.exists(log_file_path): + return render_template('logs.html', + logs=[], + error_message=f"Log file not found: {log_file_path}") + + try: + logs = parse_log_file(log_file_path) + if not logs: + return render_template('logs.html', + logs=[], + error_message="Log file is empty or unreadable") + return render_template('logs.html', logs=logs) + except Exception as e: + return render_template('logs.html', + logs=[], + error_message=f"Error parsing logs: {str(e)}") + @app.route('/home') def home(): diff --git a/static/js/logs.js b/static/js/logs.js index 23045ec..15619f6 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -6,6 +6,8 @@ document.addEventListener('DOMContentLoaded', function() { 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 diff --git a/templates/index.html b/templates/index.html index 6fdd959..41454d2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,15 @@ {% block title %}HAProxy • Configuration{% endblock %} -{% block breadcrumb %}Configuration{% endblock %} +{% block breadcrumb %} + +{% endblock %} + {% block content %} diff --git a/templates/logs.html b/templates/logs.html index 8c1b1f2..3ffb1dc 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -4,17 +4,34 @@ {% block title %}HAProxy • Logs{% endblock %} -{% block breadcrumb %}Logs{% endblock %} +{% block breadcrumb %} + +{% endblock %} {% block content %}
-
+
HAProxy Access Logs
- {% if logs %} + + {% if error_message %} + + {% endif %} + + + {% if logs and logs|length > 0 %}
@@ -161,9 +178,25 @@
- {% else %} + {% elif logs %} +
- No log entries found. + No log entries match your filters. +
+ {% else %} + + {% endif %} @@ -171,4 +204,5 @@
+ {% endblock %} From 9fae35fe8a4a339cf9866bc599717a41510fb0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:33:23 +0100 Subject: [PATCH 13/33] new options --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 3538ed0..8424368 100644 --- a/app.py +++ b/app.py @@ -102,7 +102,7 @@ def display_haproxy_stats(): @app.route('/logs') -@requires_auth +#@requires_auth def display_haproxy_logs(): log_file_path = '/var/log/haproxy.log' From c838521adc894865238d206530724d4e8dd481b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:34:18 +0100 Subject: [PATCH 14/33] new options --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 8424368..bfa45cd 100644 --- a/app.py +++ b/app.py @@ -101,7 +101,7 @@ def display_haproxy_stats(): return render_template('statistics.html', stats=parsed_stats) -@app.route('/logs') +@app.route('/logs', endpoint='display_logs') #@requires_auth def display_haproxy_logs(): log_file_path = '/var/log/haproxy.log' From 45d8634f080c5ea3756ac5f9d1a77ac52b6db50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 10:38:40 +0100 Subject: [PATCH 15/33] new options --- static/js/logs.js | 13 +++++++++++-- templates/edit.html | 2 +- templates/logs.html | 15 +++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/static/js/logs.js b/static/js/logs.js index 15619f6..d0d790f 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -3,6 +3,7 @@ document.addEventListener('DOMContentLoaded', function() { 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'); @@ -16,6 +17,7 @@ document.addEventListener('DOMContentLoaded', function() { const statusValue = filterStatus.value; const methodValue = filterMethod.value; const showThreats = filterThreats.checked; + const hideStats = filterHideStats.checked; let visibleCount = 0; let threatCount = 0; @@ -27,6 +29,7 @@ document.addEventListener('DOMContentLoaded', function() { 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; @@ -53,6 +56,11 @@ document.addEventListener('DOMContentLoaded', function() { show = false; } + // Hide /stats filter + if (hideStats && url.includes('/stats')) { + show = false; + } + row.style.display = show ? '' : 'none'; if (show) { @@ -79,6 +87,7 @@ document.addEventListener('DOMContentLoaded', function() { filterStatus.addEventListener('change', applyFilters); filterMethod.addEventListener('change', applyFilters); filterThreats.addEventListener('change', applyFilters); + filterHideStats.addEventListener('change', applyFilters); // Reset button resetBtn.addEventListener('click', function() { @@ -86,9 +95,9 @@ document.addEventListener('DOMContentLoaded', function() { filterStatus.value = ''; filterMethod.value = ''; filterThreats.checked = true; + filterHideStats.checked = true; applyFilters(); }); - // Initial stats applyFilters(); -}); \ No newline at end of file +}); diff --git a/templates/edit.html b/templates/edit.html index 5f137ce..ccc9a39 100644 --- a/templates/edit.html +++ b/templates/edit.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% set active_page = "" %} {% block title %}HAProxy • Edit{% endblock %} -{% block breadcrumb %}{% endblock %} +{% block breadcrumb %}{% endblock %} {% block content %}
diff --git a/templates/logs.html b/templates/logs.html index 3ffb1dc..53c3f82 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -16,12 +16,11 @@ {% block content %}
-
+
HAProxy Access Logs
- {% if error_message %} {% endif %} - {% if logs and logs|length > 0 %}
@@ -62,12 +60,19 @@
+
+
+ + +
+
-
@@ -179,12 +184,10 @@
{% elif logs %} -
No log entries match your filters.
{% else %} - @@ -91,7 +91,7 @@
+ placeholder="e.g. host.domain.com" required>
Domain name for the ACL rule - traffic will be matched by Host header
From 84d7139c155321d7d81b14e20cfb9527cff3b270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 11:14:41 +0100 Subject: [PATCH 21/33] new options --- routes/main_routes.py | 59 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index f3e92e5..42899ca 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -1,9 +1,42 @@ -from flask import Blueprint, render_template, request +from flask import Blueprint, render_template, request, flash +import subprocess from auth.auth_middleware import requires_auth from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends main_bp = Blueprint('main', __name__) +def reload_haproxy(): + """Reload HAProxy via supervisorctl""" + try: + result = subprocess.run( + ['/usr/sbin/haproxy', '-f', '/etc/haproxy/haproxy.cfg', '-c'], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode != 0: + print(f"[HAPROXY] Config validation failed: {result.stderr}", flush=True) + return False, "Config validation failed" + + result = subprocess.run( + ['/usr/bin/supervisorctl', 'restart', 'haproxy'], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + print(f"[HAPROXY] Restarted successfully via supervisorctl", flush=True) + return True, "HAProxy restarted successfully" + else: + print(f"[HAPROXY] Restart failed: {result.stderr}", flush=True) + return False, f"Restart failed: {result.stderr}" + + except Exception as e: + print(f"[HAPROXY] Error reloading: {e}", flush=True) + return False, f"Error: {e}" + @main_bp.route('/', methods=['GET', 'POST']) @requires_auth def index(): @@ -35,7 +68,7 @@ def index(): backend_server_ports = request.form.getlist('backend_server_ports[]') backend_server_maxconns = request.form.getlist('backend_server_maxconns[]') - # Custom ACL (NEW) + # Custom ACL add_custom_acl = 'add_custom_acl' in request.form custom_acl_name = request.form.get('custom_acl_name', '').strip() if add_custom_acl else '' custom_acl_type = request.form.get('custom_acl_type', 'path_beg') if add_custom_acl else '' @@ -88,13 +121,12 @@ def index(): ip = backend_server_ips[i] if i < len(backend_server_ips) else '' port = backend_server_ports[i] if i < len(backend_server_ports) else '' maxconn = backend_server_maxconns[i] if i < len(backend_server_maxconns) else None - if ip and port: backend_servers.append((name, ip, port, maxconn)) # Validate frontend existence if is_frontend_exist(frontend_name, frontend_ip, frontend_port): - return render_template('index.html', + return render_template('index.html', message="Frontend or Port already exists. Cannot add duplicate.", message_type="danger") @@ -177,14 +209,23 @@ def index(): custom_acl_redirect_url=custom_acl_redirect_url ) - # Determine message type - message_type = "success" if "successfully" in message else "danger" + # ===== RELOAD HAPROXY ===== + message_type = "success" if "successfully" in message.lower() else "danger" - return render_template('index.html', + 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 + + # GET request - display stats frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends() return render_template('index.html', From ca39dd35aefc87d59489668b48b0b59a4d89447a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 11:20:55 +0100 Subject: [PATCH 22/33] new options --- routes/main_routes.py | 18 +++++++++++------- supervisord.conf | 17 ++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index 42899ca..1e7105a 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -5,8 +5,8 @@ from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count main_bp = Blueprint('main', __name__) + def reload_haproxy(): - """Reload HAProxy via supervisorctl""" try: result = subprocess.run( ['/usr/sbin/haproxy', '-f', '/etc/haproxy/haproxy.cfg', '-c'], @@ -17,25 +17,29 @@ def reload_haproxy(): if result.returncode != 0: print(f"[HAPROXY] Config validation failed: {result.stderr}", flush=True) - return False, "Config validation failed" + return False, f"Config validation failed: {result.stderr}" + + print("[HAPROXY] Config valid, attempting restart via supervisorctl...", flush=True) result = subprocess.run( ['/usr/bin/supervisorctl', 'restart', 'haproxy'], capture_output=True, text=True, - timeout=10 + timeout=15 ) if result.returncode == 0: print(f"[HAPROXY] Restarted successfully via supervisorctl", flush=True) return True, "HAProxy restarted successfully" else: - print(f"[HAPROXY] Restart failed: {result.stderr}", flush=True) - return False, f"Restart failed: {result.stderr}" + stderr = result.stderr or result.stdout + print(f"[HAPROXY] Restart failed: {stderr}", flush=True) + return False, f"Restart failed: {stderr}" except Exception as e: - print(f"[HAPROXY] Error reloading: {e}", flush=True) - return False, f"Error: {e}" + print(f"[HAPROXY] Error: {e}", flush=True) + return False, f"Error: {str(e)}" + @main_bp.route('/', methods=['GET', 'POST']) @requires_auth diff --git a/supervisord.conf b/supervisord.conf index 78e2b56..64f265b 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -5,6 +5,16 @@ loglevel=info logfile=/var/log/supervisor/supervisord.log pidfile=/var/run/supervisord.pid +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + [program:haproxy] command=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg autostart=true @@ -28,10 +38,3 @@ priority=999 environment=FLASK_APP=/app/app.py,FLASK_ENV=production,PYTHONUNBUFFERED=1 startsecs=10 stopasgroup=true - -[unix_http_server] -file=/var/run/supervisor.sock -chmod=0700 - -[supervisorctl] -serverurl=unix:///var/run/supervisor.sock From f082495a13a3c5a5b2b882d1cb2180a4039aa59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 11:43:02 +0100 Subject: [PATCH 23/33] new options --- routes/main_routes.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index 1e7105a..4b5b0ec 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -5,36 +5,38 @@ from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count main_bp = Blueprint('main', __name__) +import subprocess def reload_haproxy(): + """Reload HAProxy by killing it - supervisord restarts automatically""" try: + # Validate config first result = subprocess.run( - ['/usr/sbin/haproxy', '-f', '/etc/haproxy/haproxy.cfg', '-c'], - capture_output=True, + ['haproxy', '-c', '-V', '-f', '/etc/haproxy/haproxy.cfg'], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=10 ) if result.returncode != 0: - print(f"[HAPROXY] Config validation failed: {result.stderr}", flush=True) - return False, f"Config validation failed: {result.stderr}" - - print("[HAPROXY] Config valid, attempting restart via supervisorctl...", flush=True) + return False, f"Config validation failed: {result.stdout}" + # Kill haproxy - supervisord will restart it automatically result = subprocess.run( - ['/usr/bin/supervisorctl', 'restart', 'haproxy'], - capture_output=True, + ['pkill', '-f', 'haproxy'], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, - timeout=15 + timeout=10 ) - if result.returncode == 0: - print(f"[HAPROXY] Restarted successfully via supervisorctl", flush=True) + 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: - stderr = result.stderr or result.stdout - print(f"[HAPROXY] Restart failed: {stderr}", flush=True) - return False, f"Restart failed: {stderr}" + 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) From 7b49105ba3c201943757a31705fbf5f65b16aa00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 11:53:11 +0100 Subject: [PATCH 24/33] new options --- utils/haproxy_config.py | 56 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index cd1c276..3691ae7 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -93,8 +93,11 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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." + # Generate unique backend name with hostname suffix + unique_backend_name = f"{backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else backend_name + + if is_backend_exist(unique_backend_name): + return f"Backend {unique_backend_name} already exists. Cannot add duplicate." is_no_lb = lb_method == 'no-lb' if is_no_lb and len(backend_servers) > 1: @@ -115,21 +118,14 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write(f" ssl crt {ssl_cert_path}") haproxy_cfg.write("\n") - - # ===== HTTP-REQUEST RULES (BEFORE REDIRECT) ===== - if is_no_lb: - 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") + # ===== SET HEADERS (RIGHT AFTER 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" balance {lb_method}\n") - - if forward_for: - haproxy_cfg.write(f" option forwardfor\n") - + haproxy_cfg.write(f" http-request set-header X-Forwarded-Proto http\n") + # Mode haproxy_cfg.write(f" mode {protocol}\n") @@ -138,7 +134,13 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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") - + + # Balance settings for non-no-lb mode + if not is_no_lb: + haproxy_cfg.write(f" balance {lb_method}\n") + if forward_for: + haproxy_cfg.write(f" option forwardfor\n") + # DOS protection (BEFORE REDIRECT!) if is_dos: haproxy_cfg.write(f" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n") @@ -200,14 +202,12 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, # ===== BACKEND ROUTING ===== if acl_name_sanitized: - # Jeśli jest hostname, routuj z ACL - haproxy_cfg.write(f" use_backend {backend_name} if {acl_name_sanitized}\n") + haproxy_cfg.write(f" use_backend {unique_backend_name} if {acl_name_sanitized}\n") else: - # Default backend - haproxy_cfg.write(f" default_backend {backend_name}\n") + haproxy_cfg.write(f" default_backend {unique_backend_name}\n") - # ===== PRIMARY BACKEND ===== - haproxy_cfg.write(f"\nbackend {backend_name}\n") + # ===== PRIMARY BACKEND (WITH UNIQUE NAME) ===== + haproxy_cfg.write(f"\nbackend {unique_backend_name}\n") if not is_no_lb: haproxy_cfg.write(f" balance {lb_method}\n") @@ -241,8 +241,10 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, # ===== REDIRECT FRONTEND (HTTP -> HTTPS) ===== if backend_ssl_redirect and ssl_redirect_backend_name: - if is_backend_exist(ssl_redirect_backend_name): - return f"Redirect backend {ssl_redirect_backend_name} already exists. Cannot add duplicate." + unique_redirect_backend_name = f"{ssl_redirect_backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else ssl_redirect_backend_name + + if is_backend_exist(unique_redirect_backend_name): + return f"Redirect backend {unique_redirect_backend_name} already exists. Cannot add duplicate." # Generate unique name for redirect frontend redirect_frontend_name = f"redirect_https_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"redirect_https_{frontend_name}" @@ -255,12 +257,12 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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 {ssl_redirect_backend_name} if {acl_name_redirect}\n") + haproxy_cfg.write(f" use_backend {unique_redirect_backend_name} if {acl_name_redirect}\n") else: - haproxy_cfg.write(f" default_backend {ssl_redirect_backend_name}\n") + haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n") # Redirect backend - haproxy_cfg.write(f"\nbackend {ssl_redirect_backend_name}\n") + 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") From 86de6f24bd9896e32a4d511feab626cf4a4d29b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:00:05 +0100 Subject: [PATCH 25/33] new options --- utils/haproxy_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index 3691ae7..ad6505c 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -241,7 +241,7 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, # ===== REDIRECT FRONTEND (HTTP -> HTTPS) ===== if backend_ssl_redirect and ssl_redirect_backend_name: - unique_redirect_backend_name = f"{ssl_redirect_backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else ssl_redirect_backend_name + unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"{ssl_redirect_backend_name}_redirect" if is_backend_exist(unique_redirect_backend_name): return f"Redirect backend {unique_redirect_backend_name} already exists. Cannot add duplicate." From c7a09171e1cbb81074a2d1ba9e1992272b0244cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:08:32 +0100 Subject: [PATCH 26/33] new options --- utils/haproxy_config.py | 217 +++++++++++++++++++++++++++++----------- 1 file changed, 156 insertions(+), 61 deletions(-) diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index ad6505c..df18e6a 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -76,9 +76,84 @@ def count_frontends_and_backends(): return frontend_count, backend_count, acl_count, layer7_count, layer4_count def sanitize_name(name): - """Convert hostname/name to valid ACL name (replace special chars with underscores)""" + """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'): + frontend_name = line.strip().split(' ', 1)[1] + 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 frontend_name + elif lines[j].strip().startswith('frontend') or lines[j].strip().startswith('backend'): + break + except Exception as e: + print(f"[HAPROXY_CONFIG] Error: {e}", flush=True) + + return None + +def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name): + """Dodaj ACL i use_backend do istniejącego frontendu""" + if not os.path.exists(HAPROXY_CFG): + return False + + try: + with open(HAPROXY_CFG, 'r') as f: + content = f.read() + + lines = content.split('\n') + + frontend_idx = -1 + for i, line in enumerate(lines): + if line.strip().startswith('frontend') and frontend_name in line: + frontend_idx = i + break + + if frontend_idx == -1: + return False + + insert_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 + break + + if insert_idx == -1: + insert_idx = len(lines) - 1 + + acl_line = f" acl {acl_name} hdr(host) -i {hostname}\n" + use_backend_line = f" use_backend {backend_name} if {acl_name}\n" + + for line in lines[frontend_idx:insert_idx]: + if acl_name in line and 'acl' in line: + return True + + lines.insert(insert_idx, use_backend_line) + lines.insert(insert_idx, acl_line) + + with open(HAPROXY_CFG, 'w') as f: + f.write('\n'.join(lines)) + + return True + except Exception as e: + print(f"[HAPROXY_CONFIG] Error adding ACL: {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, @@ -93,7 +168,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, os.makedirs(os.path.dirname(HAPROXY_CFG), exist_ok=True) - # Generate unique backend name with hostname suffix unique_backend_name = f"{backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else backend_name if is_backend_exist(unique_backend_name): @@ -104,14 +178,61 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, backend_servers = backend_servers[:1] try: + # ===== CHECK IF FRONTEND EXISTS AT PORT ===== + existing_frontend = frontend_exists_at_port(frontend_ip, frontend_port) + + if existing_frontend: + # Frontend już istnieje - dodaj tylko backend i ACL + unique_backend_name = f"{backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else backend_name + + # Utwórz backend + with open(HAPROXY_CFG, 'a') as haproxy_cfg: + haproxy_cfg.write(f"\nbackend {unique_backend_name}\n") + + if not is_no_lb: + haproxy_cfg.write(f" balance {lb_method}\n") + + if sticky_session and not is_no_lb: + if sticky_session_type == "cookie": + haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n") + elif sticky_session_type == "source": + haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n") + haproxy_cfg.write(f" stick on src\n") + + if health_check and protocol == 'http': + haproxy_cfg.write(f" option httpchk GET {health_check_link}\n") + elif health_check_tcp and protocol == 'tcp': + haproxy_cfg.write(f" option tcp-check\n") + + if add_header: + haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n") + + if del_server_header: + haproxy_cfg.write(f" http-response del-header Server\n") + + if forward_for: + haproxy_cfg.write(f" option forwardfor\n") + + for server_name, server_ip, server_port, maxconn in backend_servers: + maxconn_str = f" maxconn {maxconn}" if maxconn else "" + + if health_check and protocol == 'http': + haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str} check\n") + else: + haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n") + + acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"is_{unique_backend_name}" + add_acl_to_frontend(existing_frontend, acl_name_sanitized, frontend_hostname or 'localhost', unique_backend_name) + + return f"Backend added to existing frontend {existing_frontend}" + + # ===== TWORZENIE NOWEGO FRONTENDU ===== with open(HAPROXY_CFG, 'a') as haproxy_cfg: - # ===== PRIMARY FRONTEND ===== 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." - # Bind line haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}") if use_ssl: @@ -119,36 +240,33 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write("\n") - # ===== SET HEADERS (RIGHT AFTER BIND/CERT) ===== + # 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") - # Mode haproxy_cfg.write(f" mode {protocol}\n") - # ===== HOSTNAME ACL ===== + # ACL 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") - # Balance settings for non-no-lb mode if not is_no_lb: haproxy_cfg.write(f" balance {lb_method}\n") if forward_for: haproxy_cfg.write(f" option forwardfor\n") - # DOS protection (BEFORE REDIRECT!) + # Protections if is_dos: haproxy_cfg.write(f" stick-table type ip size 1m expire {ban_duration} store http_req_rate(1m)\n") haproxy_cfg.write(f" http-request track-sc0 src\n") haproxy_cfg.write(f" acl abuse sc_http_req_rate(0) gt {limit_requests}\n") haproxy_cfg.write(f" http-request silent-drop if abuse\n") - # SQL Injection protection (BEFORE REDIRECT!) if sql_injection_check: 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") @@ -156,63 +274,34 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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") - # XSS protection (BEFORE REDIRECT!) 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") - # Webshells protection (BEFORE REDIRECT!) 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") - # ===== CUSTOM ACL RULES (BEFORE REDIRECT!) ===== - if add_custom_acl and custom_acl_name and custom_acl_value: - # Write ACL rule based on type - if custom_acl_type == 'path_beg': - haproxy_cfg.write(f" acl {custom_acl_name} path_beg {custom_acl_value}\n") - elif custom_acl_type == 'path_end': - haproxy_cfg.write(f" acl {custom_acl_name} path_end {custom_acl_value}\n") - elif custom_acl_type == 'path_sub': - haproxy_cfg.write(f" acl {custom_acl_name} path_sub {custom_acl_value}\n") - elif custom_acl_type == 'hdr': - haproxy_cfg.write(f" acl {custom_acl_name} hdr_sub(host) -i {custom_acl_value}\n") - elif custom_acl_type == 'src': - haproxy_cfg.write(f" acl {custom_acl_name} src {custom_acl_value}\n") - elif custom_acl_type == 'method': - haproxy_cfg.write(f" acl {custom_acl_name} method {custom_acl_value}\n") - - # Apply action based on type - if custom_acl_action == 'deny': - haproxy_cfg.write(f" http-request deny if {custom_acl_name}\n") - elif custom_acl_action == 'redirect' and custom_acl_redirect_url: - haproxy_cfg.write(f" http-request redirect location {custom_acl_redirect_url} if {custom_acl_name}\n") - elif custom_acl_action == 'route' and custom_acl_backend: - haproxy_cfg.write(f" use_backend {custom_acl_backend} if {custom_acl_name}\n") - - # ===== HTTPS REDIRECT (AFTER ALL PROTECTIONS) ===== if https_redirect: haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n") - # ===== HTTP-RESPONSE RULES (AFTER REDIRECT) ===== if del_server_header: haproxy_cfg.write(f" http-response del-header Server\n") - # ===== BACKEND ROUTING ===== + # 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") - # ===== PRIMARY BACKEND (WITH UNIQUE NAME) ===== + # ===== BACKEND ===== haproxy_cfg.write(f"\nbackend {unique_backend_name}\n") if not is_no_lb: haproxy_cfg.write(f" balance {lb_method}\n") - # Sticky sessions if sticky_session and not is_no_lb: if sticky_session_type == "cookie": haproxy_cfg.write(f" cookie SERVERID insert indirect nocache\n") @@ -220,17 +309,20 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write(f" stick-table type ip size 200k expire 30m\n") haproxy_cfg.write(f" stick on src\n") - # Health checks 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") - # Custom headers if add_header: haproxy_cfg.write(f" http-response add-header {header_name} {header_value}\n") - # Add backend servers + if del_server_header: + haproxy_cfg.write(f" http-response del-header Server\n") + + if forward_for: + haproxy_cfg.write(f" option forwardfor\n") + for server_name, server_ip, server_port, maxconn in backend_servers: maxconn_str = f" maxconn {maxconn}" if maxconn else "" @@ -239,27 +331,30 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, else: haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n") - # ===== REDIRECT FRONTEND (HTTP -> HTTPS) ===== + # ===== REDIRECT HTTP -> HTTPS ===== if backend_ssl_redirect and ssl_redirect_backend_name: - unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"{ssl_redirect_backend_name}_redirect" + unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else ssl_redirect_backend_name - if is_backend_exist(unique_redirect_backend_name): - return f"Redirect backend {unique_redirect_backend_name} already exists. Cannot add duplicate." + # Check if HTTP frontend exists + existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port) - # Generate unique name for redirect frontend - redirect_frontend_name = f"redirect_https_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"redirect_https_{frontend_name}" - - haproxy_cfg.write(f"\nfrontend {redirect_frontend_name}\n") - haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n") - haproxy_cfg.write(f" mode http\n") - - # ===== HOSTNAME ACL FOR REDIRECT FRONTEND ===== - 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") + if not existing_http_frontend: + redirect_frontend_name = f"redirect_https_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"redirect_https_{frontend_name}" + + haproxy_cfg.write(f"\nfrontend {redirect_frontend_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: - haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n") + if frontend_hostname: + acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect" + add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name) # Redirect backend haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n") From 2d53843f342737b408220426248bad5fd833978f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:17:32 +0100 Subject: [PATCH 27/33] new options --- routes/main_routes.py | 21 ++-- templates/index.html | 51 ++++------ utils/haproxy_config.py | 209 ++++++++++++++++++---------------------- 3 files changed, 120 insertions(+), 161 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index 4b5b0ec..7e88104 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -1,12 +1,10 @@ -from flask import Blueprint, render_template, request, flash +from flask import Blueprint, render_template, request import subprocess from auth.auth_middleware import requires_auth -from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends +from utils.haproxy_config import update_haproxy_config, count_frontends_and_backends main_bp = Blueprint('main', __name__) -import subprocess - def reload_haproxy(): """Reload HAProxy by killing it - supervisord restarts automatically""" try: @@ -37,20 +35,19 @@ def reload_haproxy(): else: print(f"[HAPROXY] pkill failed: {result.stdout}", flush=True) return False, f"pkill failed: {result.stdout}" - except Exception as e: print(f"[HAPROXY] Error: {e}", flush=True) return False, f"Error: {str(e)}" - @main_bp.route('/', methods=['GET', 'POST']) @requires_auth def index(): if request.method == 'POST': - frontend_name = request.form['frontend_name'] + # Frontend IP i port (używane do generowania nazwy frontendu) frontend_ip = request.form['frontend_ip'] frontend_port = request.form['frontend_port'] frontend_hostname = request.form.get('frontend_hostname', '').strip() + lb_method = request.form['lb_method'] protocol = request.form['protocol'] backend_name = request.form['backend_name'] @@ -130,12 +127,6 @@ def index(): if ip and port: backend_servers.append((name, ip, port, maxconn)) - # Validate frontend existence - if is_frontend_exist(frontend_name, frontend_ip, frontend_port): - return render_template('index.html', - message="Frontend or Port already exists. Cannot add duplicate.", - message_type="danger") - # Health checks health_check = False health_check_link = "" @@ -161,6 +152,10 @@ def index(): acl_action = '' acl_backend_name = '' + # ===== GENERUJ FRONTEND NAME ZAMIAST PRZYJMOWAĆ OD USERA ===== + # frontend_name będzie generowany w haproxy_config.py + frontend_name = None # Będzie wygenerowany automatycznie + # Call update_haproxy_config message = update_haproxy_config( frontend_name=frontend_name, diff --git a/templates/index.html b/templates/index.html index b696289..452f664 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,7 +13,6 @@ {% endblock %} - {% block content %}
@@ -67,32 +66,24 @@
-
Frontend
+
Frontend Configuration
- - -
-
- +
- +
-
- -
-
- +
+ -
Domain name for the ACL rule - traffic will be matched by Host header
+ Frontend name will be generated automatically
@@ -114,7 +105,6 @@ -
Choose load balancing algorithm or simple single host
@@ -135,7 +125,7 @@ -
Full path to .pem file, upload certs in /ssl/
+ Full path to .pem file
@@ -157,24 +147,23 @@ -
Creates additional frontend on port 80 to redirect HTTP to HTTPS backend
+ Creates additional frontend on port 80
- + -
Name for the redirect backend that will push traffic to HTTPS
+ name="ssl_redirect_backend_name" placeholder="e.g. redirect">

-
Backend
+
Backend Configuration
@@ -297,9 +286,7 @@ -
- Adds: http-response del-header Server (security) -
+ Adds: http-response del-header Server
@@ -336,7 +323,7 @@ -
e.g. 30m, 1h, 24h
+ e.g. 30m, 1h, 24h
@@ -398,7 +385,7 @@
- +
Custom ACL Rules (Advanced)
@@ -408,18 +395,17 @@ -
Create additional routing or blocking rules beyond hostname matching
+ Create additional routing or blocking rules
- +
-
Unique identifier for this rule
@@ -437,8 +423,7 @@
-
Value to match (path, header name, IP, method)
+ placeholder="e.g. /admin, api, 192.168.1.0/24">
@@ -454,14 +439,12 @@ -
Backend to send matching traffic to
-
URL to redirect matching requests to
diff --git a/utils/haproxy_config.py b/utils/haproxy_config.py index df18e6a..9b85215 100644 --- a/utils/haproxy_config.py +++ b/utils/haproxy_config.py @@ -2,29 +2,84 @@ import os HAPROXY_CFG = '/etc/haproxy/haproxy.cfg' -def is_frontend_exist(frontend_name, frontend_ip, frontend_port): +def sanitize_name(name): + """Convert hostname/name to valid ACL name""" + return name.replace('.', '_').replace('-', '_').replace('/', '_').replace(':', '_') + +def frontend_exists_at_port(frontend_ip, frontend_port): + """Check if frontend already exists at specific port""" + if not os.path.exists(HAPROXY_CFG): + return None + + try: + with open(HAPROXY_CFG, 'r') as f: + content = f.read() + lines = content.split('\n') + + for i, line in enumerate(lines): + if line.strip().startswith('frontend'): + # Szukaj bind line + for j in range(i+1, min(i+10, len(lines))): + if lines[j].strip().startswith('bind'): + bind_info = lines[j].strip().split(' ', 1)[1] + if f"{frontend_ip}:{frontend_port}" in bind_info: + return line.strip().split(' ', 1)[1] # Zwróć nazwę frontendu + elif lines[j].strip().startswith('frontend') or lines[j].strip().startswith('backend'): + break + except Exception as e: + print(f"[HAPROXY_CONFIG] Error: {e}", flush=True) + + return None + +def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name): + """Dodaj ACL i use_backend do istniejącego frontendu""" if not os.path.exists(HAPROXY_CFG): return False try: - with open(HAPROXY_CFG, 'r') as haproxy_cfg: - frontend_found = False - for line in haproxy_cfg: - if line.strip().startswith('frontend'): - _, existing_frontend_name = line.strip().split(' ', 1) - if existing_frontend_name.strip() == frontend_name: - frontend_found = True - else: - frontend_found = False - elif frontend_found and line.strip().startswith('bind'): - _, bind_info = line.strip().split(' ', 1) - existing_ip, existing_port = bind_info.split(':', 1) - if existing_ip.strip() == frontend_ip and existing_port.strip() == frontend_port: - return True + with open(HAPROXY_CFG, 'r') as f: + lines = f.readlines() + + # Znajdź frontend + frontend_idx = -1 + for i, line in enumerate(lines): + if 'frontend' in line and frontend_name in line: + frontend_idx = i + break + + if frontend_idx == -1: + return False + + # Sprawdź czy ACL już istnieje + for line in lines[frontend_idx:]: + if acl_name in line and 'acl' in line: + return True # Już istnieje + if line.strip().startswith('backend'): + break + + # Znajdź ostatnią linię ACL/use_backend w tym frontendzie + insert_idx = frontend_idx + 1 + for i in range(frontend_idx + 1, len(lines)): + if lines[i].strip().startswith('backend'): + insert_idx = i + break + if 'use_backend' in lines[i] or 'default_backend' in lines[i]: + insert_idx = i + 1 + + # Wstaw ACL i use_backend + acl_line = f" acl {acl_name} hdr(host) -i {hostname}\n" + use_backend_line = f" use_backend {backend_name} if {acl_name}\n" + + lines.insert(insert_idx, use_backend_line) + lines.insert(insert_idx, acl_line) + + with open(HAPROXY_CFG, 'w') as f: + f.writelines(lines) + + return True except Exception as e: - print(f"[HAPROXY_CONFIG] Error checking frontend: {e}", flush=True) - - return False + print(f"[HAPROXY_CONFIG] Error adding ACL: {e}", flush=True) + return False def is_backend_exist(backend_name): if not os.path.exists(HAPROXY_CFG): @@ -75,85 +130,6 @@ def count_frontends_and_backends(): return frontend_count, backend_count, acl_count, layer7_count, layer4_count -def sanitize_name(name): - """Convert hostname/name to valid ACL name""" - return name.replace('.', '_').replace('-', '_').replace('/', '_').replace(':', '_') - -def frontend_exists_at_port(frontend_ip, frontend_port): - """Check if frontend already exists at specific port""" - if not os.path.exists(HAPROXY_CFG): - return None - - try: - with open(HAPROXY_CFG, 'r') as f: - content = f.read() - lines = content.split('\n') - - for i, line in enumerate(lines): - if line.strip().startswith('frontend'): - frontend_name = line.strip().split(' ', 1)[1] - 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 frontend_name - elif lines[j].strip().startswith('frontend') or lines[j].strip().startswith('backend'): - break - except Exception as e: - print(f"[HAPROXY_CONFIG] Error: {e}", flush=True) - - return None - -def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name): - """Dodaj ACL i use_backend do istniejącego frontendu""" - if not os.path.exists(HAPROXY_CFG): - return False - - try: - with open(HAPROXY_CFG, 'r') as f: - content = f.read() - - lines = content.split('\n') - - frontend_idx = -1 - for i, line in enumerate(lines): - if line.strip().startswith('frontend') and frontend_name in line: - frontend_idx = i - break - - if frontend_idx == -1: - return False - - insert_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 - break - - if insert_idx == -1: - insert_idx = len(lines) - 1 - - acl_line = f" acl {acl_name} hdr(host) -i {hostname}\n" - use_backend_line = f" use_backend {backend_name} if {acl_name}\n" - - for line in lines[frontend_idx:insert_idx]: - if acl_name in line and 'acl' in line: - return True - - lines.insert(insert_idx, use_backend_line) - lines.insert(insert_idx, acl_line) - - with open(HAPROXY_CFG, 'w') as f: - f.write('\n'.join(lines)) - - return True - except Exception as e: - print(f"[HAPROXY_CONFIG] Error adding ACL: {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, @@ -182,11 +158,11 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, existing_frontend = frontend_exists_at_port(frontend_ip, frontend_port) if existing_frontend: - # Frontend już istnieje - dodaj tylko backend i ACL - unique_backend_name = f"{backend_name}_{sanitize_name(frontend_hostname)}" if frontend_hostname else backend_name + # Frontend już istnieje - dodaj tylko backend + ACL + print(f"[HAPROXY] Found existing frontend '{existing_frontend}' at {frontend_ip}:{frontend_port}", flush=True) - # Utwórz backend with open(HAPROXY_CFG, 'a') as haproxy_cfg: + # ===== BACKEND ===== haproxy_cfg.write(f"\nbackend {unique_backend_name}\n") if not is_no_lb: @@ -213,6 +189,7 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, if forward_for: haproxy_cfg.write(f" option forwardfor\n") + # Add servers for server_name, server_ip, server_port, maxconn in backend_servers: maxconn_str = f" maxconn {maxconn}" if maxconn else "" @@ -221,18 +198,22 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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 {existing_frontend}" + 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) - # ===== TWORZENIE NOWEGO FRONTENDU ===== with open(HAPROXY_CFG, 'a') as haproxy_cfg: - haproxy_cfg.write(f"\nfrontend {frontend_name}\n") - - if is_frontend_exist(frontend_name, frontend_ip, frontend_port): - return "Frontend or Port already exists. Cannot add duplicate." - + # ===== PRIMARY FRONTEND (GENERIC NAME) ===== + haproxy_cfg.write(f"\nfrontend {generic_frontend_name}\n") haproxy_cfg.write(f" bind {frontend_ip}:{frontend_port}") if use_ssl: @@ -240,7 +221,7 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write("\n") - # Headers ZARAZ PO BIND/CERT + # 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") @@ -249,7 +230,7 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, haproxy_cfg.write(f" mode {protocol}\n") - # ACL + # ACL dla pierwszego vhost acl_name_sanitized = None if frontend_hostname: acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}" @@ -331,17 +312,16 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, else: haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n") - # ===== REDIRECT HTTP -> HTTPS ===== + # ===== 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 frontend exists + # Check if HTTP redirect frontend exists existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port) if not existing_http_frontend: - redirect_frontend_name = f"redirect_https_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"redirect_https_{frontend_name}" - - haproxy_cfg.write(f"\nfrontend {redirect_frontend_name}\n") + # 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") @@ -352,6 +332,7 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method, 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) From c857258dc6f28094c954bad5164e02b537e2d618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:19:21 +0100 Subject: [PATCH 28/33] new options --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index bfa45cd..2027b2f 100644 --- a/app.py +++ b/app.py @@ -10,7 +10,7 @@ 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 -from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends +from utils.haproxy_config import update_haproxy_config, count_frontends_and_backends BASE_DIR = os.path.abspath(os.path.dirname(__file__)) From f2c9f166f6b7a4f0e99558b52679722a268a24c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:25:14 +0100 Subject: [PATCH 29/33] new options --- routes/main_routes.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/routes/main_routes.py b/routes/main_routes.py index 7e88104..4fbc5fd 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -43,7 +43,7 @@ def reload_haproxy(): @requires_auth def index(): if request.method == 'POST': - # Frontend IP i port (używane do generowania nazwy frontendu) + # Frontend IP i port frontend_ip = request.form['frontend_ip'] frontend_port = request.form['frontend_port'] frontend_hostname = request.form.get('frontend_hostname', '').strip() @@ -152,9 +152,8 @@ def index(): acl_action = '' acl_backend_name = '' - # ===== GENERUJ FRONTEND NAME ZAMIAST PRZYJMOWAĆ OD USERA ===== - # frontend_name będzie generowany w haproxy_config.py - frontend_name = None # Będzie wygenerowany automatycznie + # Frontend name (None - will be generated) + frontend_name = None # Call update_haproxy_config message = update_haproxy_config( @@ -210,9 +209,25 @@ def index(): custom_acl_redirect_url=custom_acl_redirect_url ) - # ===== RELOAD HAPROXY ===== - message_type = "success" if "successfully" in message.lower() else "danger" + # ===== 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: @@ -225,8 +240,8 @@ def index(): return render_template('index.html', message=message, message_type=message_type) - - # GET request - display stats + + # GET request - display stats frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends() return render_template('index.html', From 087d2a46c3938fd59e189ea0233c77f59ef47201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:34:08 +0100 Subject: [PATCH 30/33] new options --- static/css/edit.css | 29 ++++++++++ static/js/editor.js | 133 ++++++++++++++++++++++++++++++++++++++++++++ templates/base.html | 1 + templates/edit.html | 117 ++++++++++++++++++++++++++++---------- 4 files changed, 250 insertions(+), 30 deletions(-) create mode 100644 static/css/edit.css create mode 100644 static/js/editor.js diff --git a/static/css/edit.css b/static/css/edit.css new file mode 100644 index 0000000..6e49aaa --- /dev/null +++ b/static/css/edit.css @@ -0,0 +1,29 @@ +.CodeMirror { + height: 500px !important; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; + font-size: 13px; +} + +.CodeMirror-gutters { + background-color: #263238; + border-right: 1px solid #37474f; +} + +.CodeMirror-linenumber { + color: #546e7a; +} + +.CodeMirror-cursor { + border-left: 1px solid #fff; +} + +#edit_form button { + white-space: nowrap; +} + +@media (max-width: 768px) { + .CodeMirror { + height: 300px !important; + font-size: 12px; + } +} \ No newline at end of file diff --git a/static/js/editor.js b/static/js/editor.js new file mode 100644 index 0000000..bce5b37 --- /dev/null +++ b/static/js/editor.js @@ -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'; + } +} diff --git a/templates/base.html b/templates/base.html index ed5c4b3..29ae675 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,6 +7,7 @@ {% block title %}HAProxy Configurator{% endblock %} + {% block head %}{% endblock %} diff --git a/templates/edit.html b/templates/edit.html index ccc9a39..4fe1a13 100644 --- a/templates/edit.html +++ b/templates/edit.html @@ -1,34 +1,91 @@ {% extends "base.html" %} -{% set active_page = "" %} -{% block title %}HAProxy • Edit{% endblock %} -{% block breadcrumb %}{% endblock %} -{% block content %} -
-
-

Edit HAProxy configuration

-
-
- - -
-
- - -
-
- {% if check_output %} - - {% endif %} -
+{% set active_page = "edit" %} + +{% block title %}HAProxy • Configuration Editor{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} + + + + + + +{% if check_output %} + -{% endblock %} -{% block page_js %} - +{% endif %} + + +
+
+
HAProxy Configuration Editor
+ Real-time editor with syntax highlighting +
+ +
+
+ +
+ + + +
+ + +
+
+ + + + Cancel + +
+ + + Line 1, Col 1 | + 0 characters + +
+
+
+
+ + + + + + + + {% endblock %} From c349c8e77a709ab90d5f4825a5f9782894a4a33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 3 Nov 2025 12:38:11 +0100 Subject: [PATCH 31/33] new options --- static/css/edit.css | 49 +++++++++++++++++++++++++++++++++++++++++++++ templates/edit.html | 18 +++++++++-------- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/static/css/edit.css b/static/css/edit.css index 6e49aaa..9ae33eb 100644 --- a/static/css/edit.css +++ b/static/css/edit.css @@ -2,6 +2,7 @@ height: 500px !important; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; font-size: 13px; + border: none; } .CodeMirror-gutters { @@ -17,13 +18,61 @@ 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; } +/* Alert styling */ +.alert { + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.15); +} + +.alert-success { + background-color: #d4edda; + border-color: #c3e6cb; + color: #155724; +} + +.alert-danger { + background-color: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +.alert-warning { + background-color: #fff3cd; + border-color: #ffeaa7; + color: #856404; +} + +.alert-info { + background-color: #d1ecf1; + border-color: #bee5eb; + color: #0c5460; +} + @media (max-width: 768px) { .CodeMirror { height: 300px !important; font-size: 12px; } + + #haproxy_config { + font-size: 12px; + min-height: 300px; + } } \ No newline at end of file diff --git a/templates/edit.html b/templates/edit.html index 4fe1a13..7c12de7 100644 --- a/templates/edit.html +++ b/templates/edit.html @@ -21,7 +21,7 @@ {% if check_output %} -