diff --git a/deploy/docker/assets/etc/easyhaproxy/www/dashboard.html b/deploy/docker/assets/etc/easyhaproxy/www/dashboard.html index 8e85bbc..970fa30 100644 --- a/deploy/docker/assets/etc/easyhaproxy/www/dashboard.html +++ b/deploy/docker/assets/etc/easyhaproxy/www/dashboard.html @@ -7,26 +7,26 @@ HAProxy Monitor -
- \ No newline at end of file diff --git a/docs/reference/plugins/fastcgi.md b/docs/reference/plugins/fastcgi.md index e7fe41f..943f059 100644 --- a/docs/reference/plugins/fastcgi.md +++ b/docs/reference/plugins/fastcgi.md @@ -21,7 +21,7 @@ Automatically generates HAProxy `fcgi-app` configuration that defines required C | Option | Description | Default | |-------------------|-----------------------------------------|------------------------------------| | `enabled` | Enable/disable plugin | `true` | -| `document_root` | Document root path | `/etc/easyhaproxy/www` | +| `document_root` | Document root path | `/var/www/html` | | `script_filename` | Custom pattern for SCRIPT_FILENAME | `%[path]` (uses HAProxy's default) | | `index_file` | Default index file | `index.php` | | `path_info` | Enable PATH_INFO support | `true` | @@ -41,10 +41,10 @@ services: easyhaproxy.http.localport: 9000 easyhaproxy.http.proto: fcgi easyhaproxy.http.plugins: fastcgi - easyhaproxy.http.plugin.fastcgi.document_root: /etc/easyhaproxy/www + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html easyhaproxy.http.plugin.fastcgi.index_file: index.php volumes: - - ./app:/etc/easyhaproxy/www + - ./app:/var/www/html ``` ### Docker/Docker Compose (Unix socket) @@ -58,10 +58,10 @@ services: easyhaproxy.http.socket: /run/php/php-fpm.sock easyhaproxy.http.proto: fcgi easyhaproxy.http.plugins: fastcgi - easyhaproxy.http.plugin.fastcgi.document_root: /etc/easyhaproxy/www + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html easyhaproxy.http.plugin.fastcgi.index_file: index.php volumes: - - ./app:/etc/easyhaproxy/www + - ./app:/var/www/html - /run/php:/run/php ``` @@ -73,7 +73,7 @@ kind: Ingress metadata: annotations: easyhaproxy.plugins: "fastcgi" - easyhaproxy.plugin.fastcgi.document_root: "/etc/easyhaproxy/www" + easyhaproxy.plugin.fastcgi.document_root: "/var/www/html" easyhaproxy.plugin.fastcgi.index_file: "index.php" spec: rules: @@ -101,7 +101,7 @@ easymapping: - fastcgi plugin_config: fastcgi: - document_root: /etc/easyhaproxy/www + document_root: /var/www/html index_file: index.php path_info: true ``` @@ -111,7 +111,7 @@ easymapping: | Environment Variable | Config Key | Type | Default | Description | |----------------------------------------------|-------------------|----------|------------------------|---------------------------------------| | `EASYHAPROXY_PLUGIN_FASTCGI_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains | -| `EASYHAPROXY_PLUGIN_FASTCGI_DOCUMENT_ROOT` | `document_root` | string | `/etc/easyhaproxy/www` | Document root path | +| `EASYHAPROXY_PLUGIN_FASTCGI_DOCUMENT_ROOT` | `document_root` | string | `/var/www/html` | Document root path | | `EASYHAPROXY_PLUGIN_FASTCGI_SCRIPT_FILENAME` | `script_filename` | string | `%[path]` | Custom pattern for SCRIPT_FILENAME | | `EASYHAPROXY_PLUGIN_FASTCGI_INDEX_FILE` | `index_file` | string | `index.php` | Default index file | | `EASYHAPROXY_PLUGIN_FASTCGI_PATH_INFO` | `path_info` | boolean | `true` | Enable PATH_INFO support | @@ -121,7 +121,7 @@ easymapping: ```haproxy # Top-level fcgi-app definition (added after defaults, before frontends/backends) fcgi-app fcgi_phpapp_local - docroot /etc/easyhaproxy/www + docroot /var/www/html index index.php path-info ^(/.+\.php)(/.*)?$ diff --git a/docs/reference/volumes.md b/docs/reference/volumes.md index 35b0121..636e41e 100644 --- a/docs/reference/volumes.md +++ b/docs/reference/volumes.md @@ -64,8 +64,8 @@ All EasyHAProxy files are organized under `/etc/easyhaproxy/`. This can be custo │ ├── cloudflare_ips.lst # Optional - Cloudflare plugin │ -└── www/ # Optional - FastCGI document root - └── index.php +└── www/ # 📦 Base image - Stats dashboard + └── dashboard.html # 📦 Base image - Stats dashboard UI ``` :::tip Legend @@ -88,7 +88,7 @@ The most commonly mapped volumes for persistence and customization: | `/etc/easyhaproxy/haproxy/errors-custom/` | [Custom error pages](other.md) - custom HTTP error pages (400, 403, 500, etc.) | Optional | | `/etc/easyhaproxy/plugins/` | [Custom plugins](../guides/plugins.md) - Python plugin files | Optional | | `/etc/easyhaproxy/jwt_keys/` | [JWT public keys](plugins/jwt-validator.md) - RSA public keys for JWT validation | Optional | -| `/etc/easyhaproxy/www/` | [FastCGI document root](plugins/fastcgi.md) - PHP/FastCGI application files | Optional | +| `/etc/easyhaproxy/www/` | Stats dashboard UI - served on port `stats_port + 10000` (default `11936`) | Optional | ## Directory Details diff --git a/src/easyhaproxy/main.py b/src/easyhaproxy/main.py index 63a9843..730f1ca 100644 --- a/src/easyhaproxy/main.py +++ b/src/easyhaproxy/main.py @@ -2,6 +2,8 @@ import argparse import os import shutil import sys +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer from deepdiff import DeepDiff @@ -16,6 +18,38 @@ from functions import ( from processor import ProcessorInterface +class DashboardHandler(BaseHTTPRequestHandler): + _content: bytes | None = None + + def do_GET(self): + if self.path in ("/", "/index.html", "/dashboard.html"): + if DashboardHandler._content is None: + dashboard_path = os.path.join(Consts.www_path, "dashboard.html") + try: + with open(dashboard_path, "rb") as f: + DashboardHandler._content = f.read() + except OSError: + DashboardHandler._content = b"" + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(DashboardHandler._content))) + self.end_headers() + self.wfile.write(DashboardHandler._content) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + pass + + +def start_dashboard_server(): + server = HTTPServer(("127.0.0.1", Consts.DASHBOARD_SERVER_PORT), DashboardHandler) + t = threading.Thread(target=server.serve_forever, daemon=True) + t.start() + logger_easyhaproxy.info(f"Dashboard server listening on 127.0.0.1:{Consts.DASHBOARD_SERVER_PORT}") + + def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="easy-haproxy", @@ -146,6 +180,8 @@ def start(): os.makedirs(Consts.certs_certbot, exist_ok=True) os.makedirs(Consts.certs_haproxy, exist_ok=True) + start_dashboard_server() + processor_obj.save_config(Consts.haproxy_config) processor_obj.save_certs(Consts.certs_haproxy) certbot_certs_found = processor_obj.get_certbot_hosts() diff --git a/src/easymapping/config_generator.py b/src/easymapping/config_generator.py index 01c0524..845bca1 100644 --- a/src/easymapping/config_generator.py +++ b/src/easymapping/config_generator.py @@ -4,7 +4,7 @@ import re from jinja2 import Environment, FileSystemLoader -from functions import Functions, logger_easyhaproxy +from functions import Functions, logger_easyhaproxy, Consts from .label_handler import DockerLabelHandler @@ -92,7 +92,8 @@ class HaproxyConfigGenerator: return template.render( data=self.mapping, global_plugin_configs=self.global_plugin_configs, - defaults_plugin_configs=self.defaults_plugin_configs + defaults_plugin_configs=self.defaults_plugin_configs, + dashboard_server_port=Consts.DASHBOARD_SERVER_PORT ) def parse(self, container_metadata): diff --git a/src/functions/consts.py b/src/functions/consts.py index 0475933..83316e3 100644 --- a/src/functions/consts.py +++ b/src/functions/consts.py @@ -56,4 +56,11 @@ class Consts: @classproperty def certs_haproxy(cls): """Path to user-provided certificates directory.""" - return f"{cls.base_path}/certs/haproxy" \ No newline at end of file + return f"{cls.base_path}/certs/haproxy" + + @classproperty + def www_path(cls): + """Path to the web assets directory (dashboard, static files).""" + return f"{cls.base_path}/www" + + DASHBOARD_SERVER_PORT = 9190 \ No newline at end of file diff --git a/src/plugins/builtin/fastcgi.py b/src/plugins/builtin/fastcgi.py index f99855e..a63a5a1 100644 --- a/src/plugins/builtin/fastcgi.py +++ b/src/plugins/builtin/fastcgi.py @@ -10,7 +10,7 @@ The plugin creates: Configuration: - enabled: Enable/disable the plugin (default: true) - - document_root: Document root path (default: /etc/easyhaproxy/www) + - document_root: Document root path (default: /var/www/html) - script_filename: Pattern for SCRIPT_FILENAME (default: %[path]) - index_file: Default index file (default: index.php) - path_info: Enable PATH_INFO support (default: true) @@ -39,8 +39,6 @@ Example Kubernetes Annotation: import os import sys -from functions import Consts - # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -52,7 +50,7 @@ class FastcgiPlugin(PluginInterface): def __init__(self): self.enabled = True - self.document_root = Consts.base_path + "/www" + self.document_root = "/var/www/html" self.script_filename = "%[path]" self.index_file = "index.php" self.path_info = True diff --git a/src/templates/haproxy.cfg.j2 b/src/templates/haproxy.cfg.j2 index a4d113d..123fb7a 100644 --- a/src/templates/haproxy.cfg.j2 +++ b/src/templates/haproxy.cfg.j2 @@ -97,8 +97,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:{{ dashboard_server_port }} {% endif %} {% for o in data["easymapping"] -%} {% set mode = o["mode"] or "http" %} diff --git a/tests/conftest.py b/tests/conftest.py index 3babf7f..3aa2f7f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,13 +6,12 @@ This module provides session-wide and function-level fixtures for testing. import os import shutil -import tempfile import pytest -# Create a session-wide temporary directory for all tests -# Use a different prefix to avoid conflicts with cleanup plugin (which looks for "easyhaproxy_*") -_test_session_dir = tempfile.mkdtemp(prefix="pytest_easyhaproxy_") +# Fixed temporary directory for all tests — predictable so expected fixtures can reference it +_test_session_dir = "/tmp/easyhaproxy_test" +os.makedirs(_test_session_dir, exist_ok=True) os.environ["EASYHAPROXY_BASE_PATH"] = _test_session_dir diff --git a/tests/expected/docker.txt b/tests/expected/docker.txt index 2c56560..f2d96e5 100644 --- a/tests/expected/docker.txt +++ b/tests/expected/docker.txt @@ -42,8 +42,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:9190 frontend http_in_443 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1 diff --git a/tests/expected/services-letsencrypt.txt b/tests/expected/services-letsencrypt.txt index bbcc880..0493630 100644 --- a/tests/expected/services-letsencrypt.txt +++ b/tests/expected/services-letsencrypt.txt @@ -50,8 +50,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:9190 frontend http_in_80 bind *:80 diff --git a/tests/expected/services-multiple-hosts.txt b/tests/expected/services-multiple-hosts.txt index de48d46..7a35fb7 100644 --- a/tests/expected/services-multiple-hosts.txt +++ b/tests/expected/services-multiple-hosts.txt @@ -50,8 +50,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:9190 frontend http_in_19901 bind *:19901 diff --git a/tests/expected/ssl-loose.txt b/tests/expected/ssl-loose.txt index 0300fd0..c12e7d2 100644 --- a/tests/expected/ssl-loose.txt +++ b/tests/expected/ssl-loose.txt @@ -40,8 +40,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:9190 backend certbot_backend mode http diff --git a/tests/expected/static-cors.txt b/tests/expected/static-cors.txt index 51c2743..bc750a7 100644 --- a/tests/expected/static-cors.txt +++ b/tests/expected/static-cors.txt @@ -57,8 +57,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:9190 frontend http_in_80 bind *:80 diff --git a/tests/expected/static.txt b/tests/expected/static.txt index 0b53ab0..67d9baa 100644 --- a/tests/expected/static.txt +++ b/tests/expected/static.txt @@ -50,8 +50,14 @@ frontend dashboard mode http acl is_index path / acl is_index path /index.html - http-request return status 200 content-type "text/html" file /etc/easyhaproxy/www/dashboard.html if is_index - http-request return status 404 + acl is_dashboard path /dashboard.html + http-request set-path /dashboard.html if is_index + http-request return status 404 if !is_dashboard + default_backend srv_dashboard + +backend srv_dashboard + mode http + server Local 127.0.0.1:9190 frontend http_in_443 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1 diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1318b88..495bf91 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -948,7 +948,7 @@ class TestFastcgiPlugin: assert plugin.name == "fastcgi" assert plugin.enabled is True - assert plugin.document_root == f"{Consts.base_path}/www" + assert plugin.document_root == "/var/www/html" assert plugin.index_file == "index.php" assert plugin.path_info is True assert plugin.custom_params == {} @@ -970,7 +970,7 @@ class TestFastcgiPlugin: """Test plugin generates correct HAProxy config""" plugin = FastcgiPlugin() plugin.configure({ - "document_root": f"{Consts.base_path}/www", + "document_root": "/var/www/html", "index_file": "index.php" }) @@ -992,9 +992,9 @@ class TestFastcgiPlugin: assert len(result.global_configs) == 1 fcgi_app_def = result.global_configs[0] assert "fcgi-app fcgi_phpapp_local" in fcgi_app_def - assert f"docroot {Consts.base_path}/www" in fcgi_app_def + assert "docroot /var/www/html" in fcgi_app_def assert "index index.php" in fcgi_app_def - assert result.metadata["document_root"] == f"{Consts.base_path}/www" + assert result.metadata["document_root"] == "/var/www/html" assert result.metadata["index_file"] == "index.php" def test_fastcgi_plugin_custom_params(self):