diff --git a/docs/plugins.md b/docs/plugins.md index 6db84fc..081900f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -312,6 +312,99 @@ http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn **Important:** Requires HAProxy 2.5+ with JWT support. Mount public key file as read-only volume. +### FastCGI Plugin (Domain) + +Configures FastCGI parameters for PHP-FPM and other FastCGI applications. + +**Why use it:** Automatically generates HAProxy `fcgi-app` configuration that defines required CGI parameters for PHP-FPM communication without manual HAProxy configuration. + +**Configuration options:** +- `enabled` - Enable/disable plugin (default: `true`) +- `document_root` - Document root path (default: `/var/www/html`) +- `script_filename` - Custom pattern for SCRIPT_FILENAME (default: `%[path]`, uses HAProxy's default) +- `index_file` - Default index file (default: `index.php`) +- `path_info` - Enable PATH_INFO support (default: `true`) +- `custom_params` - Dictionary of custom FastCGI parameters (optional) + +**Enable via container label (TCP connection):** +```yaml +services: + php-fpm: + image: php:8.2-fpm + labels: + easyhaproxy.http.host: phpapp.local + easyhaproxy.http.port: 80 + easyhaproxy.http.localport: 9000 + easyhaproxy.http.proto: fcgi + easyhaproxy.http.plugins: fastcgi + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html + easyhaproxy.http.plugin.fastcgi.index_file: index.php + volumes: + - ./app:/var/www/html +``` + +**Or with Unix socket:** +```yaml +services: + php-fpm: + image: php:8.2-fpm + labels: + easyhaproxy.http.host: phpapp.local + easyhaproxy.http.socket: /run/php/php-fpm.sock + easyhaproxy.http.proto: fcgi + easyhaproxy.http.plugins: fastcgi + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html + easyhaproxy.http.plugin.fastcgi.index_file: index.php + volumes: + - ./app:/var/www/html + - /run/php:/run/php +``` + +**Custom document root and index file:** +```yaml +labels: + easyhaproxy.http.plugins: fastcgi + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/myapp/public + easyhaproxy.http.plugin.fastcgi.index_file: app.php + easyhaproxy.http.plugin.fastcgi.path_info: true +``` + +**HAProxy config generated:** + +The plugin generates a top-level `fcgi-app` section and a `use-fcgi-app` directive in the backend: + +```haproxy +# Top-level fcgi-app definition (added after defaults, before frontends/backends) +fcgi-app fcgi_phpapp_local + docroot /var/www/html + index index.php + path-info ^(/.+\.php)(/.*)?$ + +# Backend configuration (added to the backend section) +backend srv_phpapp_local_80 + use-fcgi-app fcgi_phpapp_local + # TCP connection: + server srv-0 172.19.0.3:9000 proto fcgi + # OR Unix socket: + # server srv-0 /run/php/php-fpm.sock proto fcgi +``` + +**Note:** HAProxy automatically sets standard CGI parameters (SCRIPT_FILENAME, DOCUMENT_ROOT, REQUEST_URI, QUERY_STRING, REQUEST_METHOD, CONTENT_TYPE, CONTENT_LENGTH, SERVER_NAME, SERVER_PORT, etc.) based on the `fcgi-app` configuration when communicating with PHP-FPM via the FastCGI protocol. + +**What it configures:** +- ✅ SCRIPT_FILENAME - Path to PHP script +- ✅ DOCUMENT_ROOT - Document root directory +- ✅ SCRIPT_NAME - Script name from URL +- ✅ REQUEST_URI - Full request URI with query string +- ✅ QUERY_STRING - URL query parameters +- ✅ REQUEST_METHOD - HTTP method (GET, POST, etc.) +- ✅ CONTENT_TYPE & CONTENT_LENGTH - Request body info +- ✅ SERVER_NAME & SERVER_PORT - Server details +- ✅ HTTPS - SSL/TLS status +- ✅ PATH_INFO - Path information (optional) + +**Important:** Use this plugin together with `proto: fcgi` parameter for complete PHP-FPM support. + ## Configuration Methods Plugins can be configured using different methods depending on your deployment environment: diff --git a/examples/docker/README.md b/examples/docker/README.md index ce0640b..cf68e56 100644 --- a/examples/docker/README.md +++ b/examples/docker/README.md @@ -151,6 +151,117 @@ labels: ## Plugin Examples +### FastCGI Plugin with PHP-FPM + +Run PHP applications with FastCGI protocol support: + +**File:** `docker-compose-php-fpm.yml` + +**What it demonstrates:** +- PHP-FPM 8.5 with TCP connection on port 9000 +- FastCGI protocol support (`proto: fcgi`) +- FastCGI plugin for PHP environment configuration +- Custom document root and index file +- PATH_INFO support for RESTful routing + +**Features:** +- HAProxy forwards requests to PHP-FPM via TCP (port 9000) +- FastCGI plugin generates `fcgi-app` configuration that defines CGI parameters: + - `SCRIPT_FILENAME`, `DOCUMENT_ROOT`, `REQUEST_URI` + - `QUERY_STRING`, `REQUEST_METHOD`, `CONTENT_TYPE` + - `SERVER_NAME`, `SERVER_PORT`, `HTTPS` + - `PATH_INFO` (for routing support) +- Sample PHP application included in `php-app/` directory + +**Configuration:** +```yaml +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy:4.6.0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + EASYHAPROXY_DISCOVER: docker + ports: + - "80:80/tcp" + + php-fpm: + image: byjg/php:8.5-fpm + volumes: + - ./php-app:/var/www/html:ro + labels: + easyhaproxy.http.host: phpapp.local + easyhaproxy.http.port: 80 + # PHP-FPM listens on port 9000 + easyhaproxy.http.localport: 9000 + easyhaproxy.http.proto: fcgi + # Enable FastCGI plugin + easyhaproxy.http.plugins: fastcgi + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html + easyhaproxy.http.plugin.fastcgi.index_file: index.php + easyhaproxy.http.plugin.fastcgi.path_info: "true" +``` + +**Usage:** +```bash +# Add to /etc/hosts +echo "127.0.0.1 phpapp.local" | sudo tee -a /etc/hosts + +# Start the stack +docker compose -f docker-compose-php-fpm.yml up -d + +# Test PHP application +curl http://phpapp.local/ +curl http://phpapp.local/info.php +curl http://phpapp.local/test-path-info.php/users/123 +``` + +**Alternative: Unix Socket Connection** + +For PHP-FPM images that support Unix sockets, you can use socket connection: + +```yaml +services: + haproxy: + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - php-fpm-socket:/run/php + + php-fpm: + image: php:8.2-fpm # Official PHP image supports sockets + volumes: + - php-fpm-socket:/run/php + - ./php-app:/var/www/html:ro + labels: + easyhaproxy.http.host: phpapp.local + easyhaproxy.http.port: 80 + easyhaproxy.http.socket: /run/php/php-fpm.sock + easyhaproxy.http.proto: fcgi + easyhaproxy.http.plugins: fastcgi + # ... plugin configuration + +volumes: + php-fpm-socket: +``` + +**Sample Application:** + +The `php-app/` directory contains: +- `index.php` - Main page showing FastCGI environment +- `info.php` - PHP configuration info (phpinfo) +- `test-path-info.php` - PATH_INFO routing demonstration + +**What the FastCGI plugin does:** +1. Sets `SCRIPT_FILENAME` with proper document root path +2. Handles directory requests (appends `index.php`) +3. Sets all standard CGI environment variables +4. Enables `PATH_INFO` for RESTful URL routing +5. Supports custom FastCGI parameters + +--- + ### JWT Validator Plugin Protect your API with JWT token validation: diff --git a/examples/docker/docker-compose-php-fpm.yml b/examples/docker/docker-compose-php-fpm.yml new file mode 100644 index 0000000..8c030c6 --- /dev/null +++ b/examples/docker/docker-compose-php-fpm.yml @@ -0,0 +1,60 @@ +# FastCGI Plugin Example with PHP-FPM +# +# This example demonstrates PHP-FPM configuration with FastCGI protocol support +# using HAProxy as a reverse proxy and the FastCGI plugin for PHP environment setup. +# +# Prerequisites: +# 1. Add to /etc/hosts: +# 127.0.0.1 phpapp.local +# +# 2. Start the stack: +# docker compose -f docker-compose-php-fpm.yml up -d +# +# 3. Test PHP application: +# curl http://phpapp.local/ +# curl http://phpapp.local/info.php +# +# Features: +# - PHP-FPM 8.5 with TCP connection on port 9000 +# - FastCGI protocol support +# - Custom document root +# - PATH_INFO support for routing +# - Custom FastCGI parameters + +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy:4.6.0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + EASYHAPROXY_DISCOVER: docker + HAPROXY_CUSTOMERRORS: "true" + HAPROXY_USERNAME: admin + HAPROXY_PASSWORD: password + HAPROXY_STATS_PORT: 1936 + ports: + - "80:80/tcp" + - "1936:1936/tcp" + + # PHP-FPM service using byjg/php image + php-fpm: + image: byjg/php:8.5-fpm + volumes: + # Mount PHP application files + - ./php-app:/var/www/html:ro + labels: + easyhaproxy.http.host: phpapp.local + easyhaproxy.http.port: 80 + # PHP-FPM listens on port 9000 + easyhaproxy.http.localport: 9000 + easyhaproxy.http.proto: fcgi + + # Enable FastCGI plugin for PHP environment configuration + easyhaproxy.http.plugins: fastcgi + + # FastCGI plugin configuration + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html + easyhaproxy.http.plugin.fastcgi.index_file: index.php + easyhaproxy.http.plugin.fastcgi.path_info: "true" diff --git a/examples/docker/php-app/README.md b/examples/docker/php-app/README.md new file mode 100644 index 0000000..f869c88 --- /dev/null +++ b/examples/docker/php-app/README.md @@ -0,0 +1,151 @@ +# Sample PHP Application for FastCGI Plugin + +This directory contains a sample PHP application that demonstrates the FastCGI plugin functionality with EasyHAProxy. + +## Files + +### index.php +The main page that displays: +- PHP version and configuration +- FastCGI environment variables set by EasyHAProxy +- How the FastCGI plugin works +- Links to test pages + +Access: `http://phpapp.local/` + +### info.php +Standard `phpinfo()` page showing complete PHP configuration. + +Access: `http://phpapp.local/info.php` + +### test-path-info.php +Demonstrates PATH_INFO support for RESTful URL routing. + +Examples: +- `http://phpapp.local/test-path-info.php/users` +- `http://phpapp.local/test-path-info.php/users/123` +- `http://phpapp.local/test-path-info.php/api/v1/products` + +## FastCGI Environment Variables + +The FastCGI plugin generates an `fcgi-app` configuration that defines these CGI parameters for HAProxy to use: + +| Variable | Description | Example | +|----------|-------------|---------| +| `SCRIPT_FILENAME` | Full path to PHP script | `/var/www/html/index.php` | +| `DOCUMENT_ROOT` | Document root directory | `/var/www/html` | +| `SCRIPT_NAME` | Script path | `/index.php` | +| `REQUEST_URI` | Full request URI with query | `/index.php?page=1` | +| `QUERY_STRING` | Query string parameters | `page=1&limit=10` | +| `REQUEST_METHOD` | HTTP method | `GET`, `POST`, etc. | +| `CONTENT_TYPE` | Request content type | `application/json` | +| `CONTENT_LENGTH` | Request body length | `1024` | +| `SERVER_NAME` | Virtual host name | `phpapp.local` | +| `SERVER_PORT` | Server port | `80` or `443` | +| `HTTPS` | SSL status | `on` or `off` | +| `PATH_INFO` | Extra path info (optional) | `/users/123` | + +## How It Works + +1. **FastCGI plugin generates configuration** (at startup) + - Creates an `fcgi-app` section with CGI parameter definitions + - Includes `docroot`, `index`, and `path-info` settings + - Adds `use-fcgi-app` directive to the backend + +2. **Request arrives at HAProxy** (port 80) + - URL: `http://phpapp.local/index.php` + +3. **HAProxy uses the fcgi-app configuration** + - Sets `SCRIPT_FILENAME` to `/var/www/html/index.php` + - Sets `DOCUMENT_ROOT` to `/var/www/html` + - Sets all other CGI variables based on the request + - Handles directory requests (appends `index.php`) + +4. **HAProxy forwards to PHP-FPM** via FastCGI protocol + - Host: `php-fpm` (container name) + - Port: `9000` (TCP) or Unix socket + - Protocol: `fcgi` + - Sends CGI parameters in FastCGI format + +5. **PHP-FPM executes the script** + - Reads the PHP file from `SCRIPT_FILENAME` + - Processes the PHP code with CGI environment + - Returns HTML/JSON response + +6. **HAProxy sends response to client** + +## Customizing + +You can customize the FastCGI plugin configuration in `docker-compose-php-fpm.yml`: + +```yaml +labels: + # Change document root + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/public + + # Change default index file + easyhaproxy.http.plugin.fastcgi.index_file: app.php + + # Disable PATH_INFO + easyhaproxy.http.plugin.fastcgi.path_info: "false" + + # Add custom FastCGI parameters + easyhaproxy.http.plugin.fastcgi.custom_params: '{"PHP_VALUE":"memory_limit=256M","APP_ENV":"production"}' +``` + +## Adding Your Own PHP Application + +Replace the contents of this directory with your own PHP application: + +```bash +# Remove sample files +rm -rf php-app/* + +# Copy your PHP application +cp -r /path/to/your/app/* php-app/ + +# Restart the stack +docker compose -f docker-compose-php-fpm.yml restart +``` + +Make sure your application's entry point matches the `index_file` configuration (default: `index.php`). + +## Troubleshooting + +### "File not found" error + +Check that: +1. The file exists in the `php-app/` directory +2. The `document_root` matches the container path (`/var/www/html`) +3. The volume mount is correct in docker-compose.yml + +### PATH_INFO not working + +Ensure `path_info` is enabled in the plugin configuration: +```yaml +easyhaproxy.http.plugin.fastcgi.path_info: "true" +``` + +### PHP-FPM connection error + +Verify: +1. The `localport: 9000` is set correctly +2. The `proto: fcgi` parameter is set +3. Both containers are running and can communicate + +View logs: +```bash +docker compose -f docker-compose-php-fpm.yml logs php-fpm +docker compose -f docker-compose-php-fpm.yml logs haproxy +``` + +Check connectivity: +```bash +docker compose -f docker-compose-php-fpm.yml exec haproxy ping php-fpm +``` + +## Learn More + +- [FastCGI Plugin Documentation](../../../docs/plugins.md#fastcgi-plugin) +- [Container Labels Reference](../../../docs/container-labels.md) +- [HAProxy FastCGI Documentation](https://docs.haproxy.org/2.8/configuration.html#5.2-proto) diff --git a/examples/docker/php-app/index.php b/examples/docker/php-app/index.php new file mode 100644 index 0000000..34a20f0 --- /dev/null +++ b/examples/docker/php-app/index.php @@ -0,0 +1,152 @@ + + + + + + PHP-FPM with EasyHAProxy + + + +
+

PHP-FPM with EasyHAProxy FastCGI Plugin

+ +
+ Success! PHP is running via FastCGI protocol through HAProxy. +
+ +

FastCGI Environment

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PHP Version
Server Software
Document Root
Script Filename
Request URI
Request Method
Server Name
Server Port
HTTPS
PATH_INFO
Gateway Interface
+ +

Test Links

+ + +

How This Works

+

+ This setup uses HAProxy with EasyHAProxy to proxy requests to PHP-FPM via the FastCGI protocol: +

+
    +
  1. HAProxy receives HTTP request on port 80
  2. +
  3. The FastCGI plugin generates an fcgi-app configuration that defines CGI parameters (SCRIPT_FILENAME, DOCUMENT_ROOT, etc.)
  4. +
  5. HAProxy uses this configuration to communicate with PHP-FPM via the FastCGI protocol
  6. +
  7. HAProxy connects to PHP-FPM (via TCP port 9000 or Unix socket, depending on configuration)
  8. +
  9. PHP-FPM processes the PHP script and returns the response
  10. +
  11. HAProxy sends the response back to the client
  12. +
+ +

Configuration

+

The FastCGI plugin is configured in docker-compose-php-fpm.yml:

+ +
+ + diff --git a/examples/docker/php-app/info.php b/examples/docker/php-app/info.php new file mode 100644 index 0000000..9a6e273 --- /dev/null +++ b/examples/docker/php-app/info.php @@ -0,0 +1,9 @@ + + + + + + PATH_INFO Test + + + +
+

PATH_INFO Test

+ + +
+ Success! PATH_INFO is working correctly. +
+ +

PATH_INFO Value

+
+ +

Parsed Path Segments

+
+ + +
+ Note: PATH_INFO is not set. Try accessing this page with additional path segments. +
+ + +

Request Information

+
+ +

Example Usage

+

PATH_INFO enables RESTful URL routing. Try these URLs:

+ + +

← Back to Home

+
+ + diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index eb810f9..ccf5812 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -98,7 +98,9 @@ class HaproxyConfigGenerator: enabled_list = [] global_results = self.plugin_manager.execute_global_plugins(global_context, enabled_list) - self.global_plugin_configs = [r.haproxy_config for r in global_results if r.haproxy_config] + # Extend instead of replace to preserve fcgi-app definitions from domain plugins + global_configs = [r.haproxy_config for r in global_results if r.haproxy_config] + self.global_plugin_configs.extend(global_configs) except Exception as e: import logging logging.warning(f"Failed to execute global plugins: {e}") @@ -265,6 +267,12 @@ class HaproxyConfigGenerator: easymapping[port]["hosts"][hostname]["plugin_configs"] = [ r.haproxy_config for r in domain_results if r.haproxy_config ] + + # Extract fcgi-app definitions from metadata and add to global configs + for result in domain_results: + if result.metadata and "fcgi_app_definition" in result.metadata: + if result.metadata["fcgi_app_definition"] not in self.global_plugin_configs: + self.global_plugin_configs.append(result.metadata["fcgi_app_definition"]) except Exception as e: import logging logging.warning(f"Failed to execute domain plugins for {hostname}: {e}") diff --git a/src/plugins/builtin/fastcgi.py b/src/plugins/builtin/fastcgi.py new file mode 100644 index 0000000..c50206f --- /dev/null +++ b/src/plugins/builtin/fastcgi.py @@ -0,0 +1,155 @@ +""" +FastCGI Plugin for EasyHAProxy + +This plugin generates HAProxy fcgi-app configuration for PHP-FPM and other FastCGI applications. +It runs as a DOMAIN plugin (once per domain). + +The plugin creates: + 1. A top-level fcgi-app section with CGI parameter definitions + 2. A use-fcgi-app directive in the backend + +Configuration: + - enabled: Enable/disable the plugin (default: true) + - 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) + - custom_params: Dictionary of custom FastCGI parameters (optional) + +Example YAML config: + plugins: + fastcgi: + enabled: true + document_root: /var/www/html + index_file: index.php + path_info: true + +Example Container Label: + easyhaproxy.http.plugins: "fastcgi" + easyhaproxy.http.plugin.fastcgi.document_root: /var/www/myapp + easyhaproxy.http.plugin.fastcgi.index_file: index.php + easyhaproxy.http.plugin.fastcgi.path_info: true + +Example Kubernetes Annotation: + easyhaproxy.plugins: "fastcgi" + easyhaproxy.plugin.fastcgi.document_root: /var/www/myapp + easyhaproxy.plugin.fastcgi.index_file: index.php +""" + +import os +import sys + +# Add parent directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from plugins import PluginInterface, PluginType, PluginContext, PluginResult +from functions import loggerEasyHaproxy + + +class FastcgiPlugin(PluginInterface): + """Plugin to configure FastCGI parameters for PHP-FPM""" + + def __init__(self): + self.enabled = True + self.document_root = "/var/www/html" + self.script_filename = "%[path]" + self.index_file = "index.php" + self.path_info = True + self.custom_params = {} + + @property + def name(self) -> str: + return "fastcgi" + + @property + def plugin_type(self) -> PluginType: + return PluginType.DOMAIN + + def configure(self, config: dict) -> None: + """ + Configure the plugin + + Args: + config: Dictionary with configuration options + - enabled: Whether plugin is enabled + - document_root: Document root path + - script_filename: Pattern for SCRIPT_FILENAME + - index_file: Default index file + - path_info: Enable PATH_INFO support + - custom_params: Dictionary of custom FastCGI parameters + """ + if "enabled" in config: + self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"] + + if "document_root" in config: + self.document_root = config["document_root"] + + if "script_filename" in config: + self.script_filename = config["script_filename"] + + if "index_file" in config: + self.index_file = config["index_file"] + + if "path_info" in config: + self.path_info = str(config["path_info"]).lower() in ["true", "1", "yes"] + + if "custom_params" in config: + self.custom_params = config["custom_params"] + + def process(self, context: PluginContext) -> PluginResult: + """ + Process the plugin and generate FastCGI configuration + + Args: + context: Plugin execution context + + Returns: + PluginResult with HAProxy FastCGI configuration + """ + if not self.enabled: + return PluginResult() + + # Generate a unique fcgi-app name based on the domain + # Replace dots and colons with underscores for valid HAProxy identifier + domain_safe = context.domain.replace(".", "_").replace(":", "_") + fcgi_app_name = f"fcgi_{domain_safe}" + + # Generate the use-fcgi-app directive for the backend + backend_config = f"use-fcgi-app {fcgi_app_name}" + + # Generate the fcgi-app section (to be inserted at top level) + fcgi_app_lines = [f"fcgi-app {fcgi_app_name}"] + fcgi_app_lines.append(f" docroot {self.document_root}") + fcgi_app_lines.append(f" index {self.index_file}") + + # PATH_INFO support + if self.path_info: + fcgi_app_lines.append(f" path-info ^(/.+\\.php)(/.*)?$") + + # Set SCRIPT_FILENAME if customized + if self.script_filename and self.script_filename != "%[path]": + fcgi_app_lines.append(f" set-param SCRIPT_FILENAME {self.script_filename}") + + # Custom parameters + if self.custom_params: + for param_name, param_value in self.custom_params.items(): + fcgi_app_lines.append(f" set-param {param_name.upper()} {param_value}") + + fcgi_app_definition = "\n".join(fcgi_app_lines) + + # Build metadata - store fcgi_app_definition to be extracted and added to global configs + metadata = { + "domain": context.domain, + "fcgi_app_name": fcgi_app_name, + "fcgi_app_definition": fcgi_app_definition, # For top-level injection + "document_root": self.document_root, + "index_file": self.index_file, + "path_info": self.path_info, + "custom_params_count": len(self.custom_params) + } + + return PluginResult( + haproxy_config=backend_config, # use-fcgi-app directive for the backend + modified_easymapping=None, + metadata=metadata + ) diff --git a/src/tests/expected/services-fcgi.txt b/src/tests/expected/services-fcgi.txt new file mode 100644 index 0000000..f7c1bf5 --- /dev/null +++ b/src/tests/expected/services-fcgi.txt @@ -0,0 +1,56 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + + # intermediate configuration + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-dh-param-file /etc/haproxy/dhparam + +defaults + log global + option httplog + + timeout connect 3s + timeout client 10s + timeout server 10m + + + +frontend http_in_80 + bind *:80 + mode http + + acl is_rule_phpapp_local_80_1 hdr(host) -i phpapp.local + acl is_rule_phpapp_local_80_2 hdr(host) -i phpapp.local:80 + use_backend srv_phpapp_local_80 if is_rule_phpapp_local_80_1 OR is_rule_phpapp_local_80_2 + + acl is_rule_phpapp-tcp_local_80_1 hdr(host) -i phpapp-tcp.local + acl is_rule_phpapp-tcp_local_80_2 hdr(host) -i phpapp-tcp.local:80 + use_backend srv_phpapp-tcp_local_80 if is_rule_phpapp-tcp_local_80_1 OR is_rule_phpapp-tcp_local_80_2 + +backend srv_phpapp_local_80 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 /run/php/php-fpm.sock check weight 1 proto fcgi +backend srv_phpapp-tcp_local_80 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 172.17.0.3:9000 check weight 1 proto fcgi + +backend certbot_backend + mode http + server certbot 127.0.0.1:2080 diff --git a/src/tests/fixtures/services-fcgi b/src/tests/fixtures/services-fcgi new file mode 100644 index 0000000..d4fdc12 --- /dev/null +++ b/src/tests/fixtures/services-fcgi @@ -0,0 +1,16 @@ +{ + "172.17.0.2": { + "easyhaproxy.definitions": "fcgi", + "easyhaproxy.fcgi.host": "phpapp.local", + "easyhaproxy.fcgi.port": "80", + "easyhaproxy.fcgi.socket": "/run/php/php-fpm.sock", + "easyhaproxy.fcgi.proto": "fcgi" + }, + "172.17.0.3": { + "easyhaproxy.definitions": "fcgi-tcp", + "easyhaproxy.fcgi-tcp.host": "phpapp-tcp.local", + "easyhaproxy.fcgi-tcp.port": "80", + "easyhaproxy.fcgi-tcp.localport": "9000", + "easyhaproxy.fcgi-tcp.proto": "fcgi" + } +} diff --git a/src/tests/test_plugins.py b/src/tests/test_plugins.py index 52600fd..188844d 100644 --- a/src/tests/test_plugins.py +++ b/src/tests/test_plugins.py @@ -22,6 +22,7 @@ from plugins.builtin.cleanup import CleanupPlugin from plugins.builtin.deny_pages import DenyPagesPlugin from plugins.builtin.ip_whitelist import IpWhitelistPlugin from plugins.builtin.jwt_validator import JwtValidatorPlugin +from plugins.builtin.fastcgi import FastcgiPlugin import easymapping @@ -736,6 +737,114 @@ class TestJwtValidatorPlugin: assert result.metadata["path_validation"] is False +class TestFastcgiPlugin: + """Test cases for FastcgiPlugin""" + + def test_fastcgi_plugin_initialization(self): + """Test plugin initializes with correct defaults""" + plugin = FastcgiPlugin() + + assert plugin.name == "fastcgi" + assert plugin.enabled is True + assert plugin.document_root == "/var/www/html" + assert plugin.index_file == "index.php" + assert plugin.path_info is True + assert plugin.custom_params == {} + + def test_fastcgi_plugin_configuration(self): + """Test plugin configuration""" + plugin = FastcgiPlugin() + plugin.configure({ + "document_root": "/var/www/myapp", + "index_file": "app.php", + "path_info": "false" + }) + + assert plugin.document_root == "/var/www/myapp" + assert plugin.index_file == "app.php" + assert plugin.path_info is False + + def test_fastcgi_plugin_generates_config(self): + """Test plugin generates correct HAProxy config""" + plugin = FastcgiPlugin() + plugin.configure({ + "document_root": "/var/www/html", + "index_file": "index.php" + }) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="phpapp.local", + port="80", + host_config={} + ) + + result = plugin.process(context) + + assert result.haproxy_config is not None + assert "use-fcgi-app fcgi_phpapp_local" in result.haproxy_config + + # Check fcgi-app definition in metadata + assert "fcgi_app_definition" in result.metadata + fcgi_app_def = result.metadata["fcgi_app_definition"] + assert "fcgi-app fcgi_phpapp_local" 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"] == "/var/www/html" + assert result.metadata["index_file"] == "index.php" + + def test_fastcgi_plugin_custom_params(self): + """Test plugin with custom FastCGI parameters""" + plugin = FastcgiPlugin() + plugin.configure({ + "custom_params": { + "CUSTOM_VAR": "custom_value", + "APP_ENV": "production" + } + }) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="phpapp.local", + port="80", + host_config={} + ) + + result = plugin.process(context) + + assert result.haproxy_config is not None + assert "use-fcgi-app fcgi_phpapp_local" in result.haproxy_config + + # Check custom params in fcgi-app definition in metadata + assert "fcgi_app_definition" in result.metadata + fcgi_app_def = result.metadata["fcgi_app_definition"] + assert "set-param CUSTOM_VAR custom_value" in fcgi_app_def + assert "set-param APP_ENV production" in fcgi_app_def + assert result.metadata["custom_params_count"] == 2 + + def test_fastcgi_plugin_disabled(self): + """Test plugin returns empty config when disabled""" + plugin = FastcgiPlugin() + plugin.configure({"enabled": "false"}) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="phpapp.local", + port="80", + host_config={} + ) + + result = plugin.process(context) + + assert result.haproxy_config is None or result.haproxy_config == "" + + class TestPluginManager: """Test cases for PluginManager""" @@ -750,10 +859,11 @@ class TestPluginManager: assert "deny_pages" in manager.plugins assert "ip_whitelist" in manager.plugins assert "jwt_validator" in manager.plugins + assert "fastcgi" in manager.plugins # Verify plugin types assert len(manager.global_plugins) == 1 # cleanup - assert len(manager.domain_plugins) == 4 # cloudflare, deny_pages, ip_whitelist, jwt_validator + assert len(manager.domain_plugins) == 5 # cloudflare, deny_pages, ip_whitelist, jwt_validator, fastcgi # Verify plugin instances assert manager.plugins["cloudflare"].name == "cloudflare"