Add pass_headers support to FastCGI plugin
This commit is contained in:
parent
91da35650d
commit
5eee2fcf19
4 changed files with 135 additions and 17 deletions
|
|
@ -19,13 +19,38 @@ Automatically generates HAProxy `fcgi-app` configuration that defines required C
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
|
||||||
| Option | Description | Default |
|
| Option | Description | Default |
|
||||||
|-------------------|-----------------------------------------|------------------------------------|
|
|-------------------|--------------------------------------------|--------------------------------------|
|
||||||
| `enabled` | Enable/disable plugin | `true` |
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
| `document_root` | Document root path | `/var/www/html` |
|
| `document_root` | Document root path | `/var/www/html` |
|
||||||
| `script_filename` | Custom pattern for SCRIPT_FILENAME | `%[path]` (uses HAProxy's default) |
|
| `script_filename` | Custom pattern for SCRIPT_FILENAME | `%[path]` (uses HAProxy's default) |
|
||||||
| `index_file` | Default index file | `index.php` |
|
| `index_file` | Default index file | `index.php` |
|
||||||
| `path_info` | Enable PATH_INFO support | `true` |
|
| `path_info` | Enable PATH_INFO support | `true` |
|
||||||
| `custom_params` | Dictionary of custom FastCGI parameters | (optional) |
|
| `custom_params` | Dictionary of custom FastCGI parameters | (optional) |
|
||||||
|
| `pass_headers` | HTTP headers to forward to the FastCGI app | (optional) |
|
||||||
|
|
||||||
|
### `pass_headers` — Forwarding HTTP Headers
|
||||||
|
|
||||||
|
:::important
|
||||||
|
HAProxy strips `Authorization`, `Proxy-Authorization`, and all hop-by-hop headers before forwarding requests to the FastCGI backend. Applications that rely on these headers (e.g. APIs using Bearer tokens, Basic auth, or JWT) will fail silently without this option.
|
||||||
|
:::
|
||||||
|
|
||||||
|
`pass_headers` accepts a comma-separated string or a list of strings/dicts:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Simple — single header
|
||||||
|
pass_headers: "Authorization"
|
||||||
|
|
||||||
|
# Multiple — comma-separated
|
||||||
|
pass_headers: "Authorization, Proxy-Authorization"
|
||||||
|
|
||||||
|
# With ACL condition — list of dicts
|
||||||
|
pass_headers:
|
||||||
|
- Authorization
|
||||||
|
- name: X-Custom-Header
|
||||||
|
condition: "if { ssl_fc }"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** `Content-Type` and `Content-Length` cannot be passed here — they are automatically converted to the CGI parameters `CONTENT_TYPE` and `CONTENT_LENGTH`.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
|
|
@ -78,6 +103,7 @@ metadata:
|
||||||
easyhaproxy.plugin.fastcgi.document_root: "/var/www/html"
|
easyhaproxy.plugin.fastcgi.document_root: "/var/www/html"
|
||||||
easyhaproxy.plugin.fastcgi.index_file: "index.php"
|
easyhaproxy.plugin.fastcgi.index_file: "index.php"
|
||||||
easyhaproxy.plugin.fastcgi.script_filename: "/var/www/html/index.php"
|
easyhaproxy.plugin.fastcgi.script_filename: "/var/www/html/index.php"
|
||||||
|
easyhaproxy.plugin.fastcgi.pass_headers: "Authorization, Proxy-Authorization"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: easyhaproxy
|
ingressClassName: easyhaproxy
|
||||||
rules:
|
rules:
|
||||||
|
|
@ -114,7 +140,7 @@ easymapping:
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Environment Variable | Config Key | Type | Default | Description |
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|----------------------------------------------|-------------------|----------|------------------------|---------------------------------------|
|
|----------------------------------------------|-------------------|----------|-------------------------|---------------------------------------|
|
||||||
| `EASYHAPROXY_PLUGIN_FASTCGI_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
| `EASYHAPROXY_PLUGIN_FASTCGI_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||||
| `EASYHAPROXY_PLUGIN_FASTCGI_DOCUMENT_ROOT` | `document_root` | string | `/var/www/html` | 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_SCRIPT_FILENAME` | `script_filename` | string | `%[path]` | Custom pattern for SCRIPT_FILENAME |
|
||||||
|
|
@ -129,6 +155,8 @@ fcgi-app fcgi_phpapp_local
|
||||||
docroot /var/www/html
|
docroot /var/www/html
|
||||||
index index.php
|
index index.php
|
||||||
path-info ^(/.+\.php)(/.*)?$
|
path-info ^(/.+\.php)(/.*)?$
|
||||||
|
pass-header Authorization
|
||||||
|
pass-header Proxy-Authorization
|
||||||
|
|
||||||
# Backend configuration (added to the backend section)
|
# Backend configuration (added to the backend section)
|
||||||
backend srv_phpapp_local_80
|
backend srv_phpapp_local_80
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ Configuration:
|
||||||
- index_file: Default index file (default: index.php)
|
- index_file: Default index file (default: index.php)
|
||||||
- path_info: Enable PATH_INFO support (default: true)
|
- path_info: Enable PATH_INFO support (default: true)
|
||||||
- custom_params: Dictionary of custom FastCGI parameters (optional)
|
- custom_params: Dictionary of custom FastCGI parameters (optional)
|
||||||
|
- pass_headers: Headers to forward to the FastCGI app (optional).
|
||||||
|
Comma-separated string or list of strings/dicts with optional condition.
|
||||||
|
|
||||||
Example YAML config:
|
Example YAML config:
|
||||||
plugins:
|
plugins:
|
||||||
|
|
@ -34,6 +36,7 @@ Example Kubernetes Annotation:
|
||||||
easyhaproxy.plugins: "fastcgi"
|
easyhaproxy.plugins: "fastcgi"
|
||||||
easyhaproxy.plugin.fastcgi.document_root: /var/www/myapp
|
easyhaproxy.plugin.fastcgi.document_root: /var/www/myapp
|
||||||
easyhaproxy.plugin.fastcgi.index_file: index.php
|
easyhaproxy.plugin.fastcgi.index_file: index.php
|
||||||
|
easyhaproxy.plugin.fastcgi.pass_headers: "Authorization, Proxy-Authorization"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -55,6 +58,7 @@ class FastcgiPlugin(PluginInterface):
|
||||||
self.index_file = "index.php"
|
self.index_file = "index.php"
|
||||||
self.path_info = True
|
self.path_info = True
|
||||||
self.custom_params = {}
|
self.custom_params = {}
|
||||||
|
self.pass_headers = [] # list of {"name": str, "condition": str|None}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
|
@ -76,6 +80,19 @@ class FastcgiPlugin(PluginInterface):
|
||||||
- index_file: Default index file
|
- index_file: Default index file
|
||||||
- path_info: Enable PATH_INFO support
|
- path_info: Enable PATH_INFO support
|
||||||
- custom_params: Dictionary of custom FastCGI parameters
|
- custom_params: Dictionary of custom FastCGI parameters
|
||||||
|
- pass_headers: Headers to forward to the FastCGI application.
|
||||||
|
HAProxy omits Authorization, Proxy-Authorization, and hop-by-hop headers
|
||||||
|
by default — this directive is required to pass them through.
|
||||||
|
Note: Content-Type and Content-Length are never passable here; they are
|
||||||
|
already converted to CGI parameters (CONTENT_TYPE, CONTENT_LENGTH).
|
||||||
|
|
||||||
|
Accepts a comma-separated string or a list of strings/dicts:
|
||||||
|
Simple: "Authorization"
|
||||||
|
Multiple: "Authorization, Proxy-Authorization"
|
||||||
|
With ACL: [{"name": "Authorization", "condition": "if { ssl_fc }"}]
|
||||||
|
|
||||||
|
HAProxy syntax: pass-header <name> [ { if | unless } <condition> ]
|
||||||
|
Ref: https://docs.haproxy.org/dev/configuration.html
|
||||||
"""
|
"""
|
||||||
if "enabled" in config:
|
if "enabled" in config:
|
||||||
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
@ -95,6 +112,23 @@ class FastcgiPlugin(PluginInterface):
|
||||||
if "custom_params" in config:
|
if "custom_params" in config:
|
||||||
self.custom_params = config["custom_params"]
|
self.custom_params = config["custom_params"]
|
||||||
|
|
||||||
|
if "pass_headers" in config:
|
||||||
|
val = config["pass_headers"]
|
||||||
|
if isinstance(val, str):
|
||||||
|
self.pass_headers = [{"name": h.strip(), "condition": None}
|
||||||
|
for h in val.split(",") if h.strip()]
|
||||||
|
elif isinstance(val, list):
|
||||||
|
result = []
|
||||||
|
for item in val:
|
||||||
|
if isinstance(item, str):
|
||||||
|
result.append({"name": item.strip(), "condition": None})
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
result.append({
|
||||||
|
"name": item["name"],
|
||||||
|
"condition": item.get("condition")
|
||||||
|
})
|
||||||
|
self.pass_headers = result
|
||||||
|
|
||||||
def process(self, context: PluginContext) -> PluginResult:
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
"""
|
"""
|
||||||
Process the plugin and generate FastCGI configuration
|
Process the plugin and generate FastCGI configuration
|
||||||
|
|
@ -134,6 +168,13 @@ class FastcgiPlugin(PluginInterface):
|
||||||
for param_name, param_value in self.custom_params.items():
|
for param_name, param_value in self.custom_params.items():
|
||||||
fcgi_app_lines.append(f" set-param {param_name.upper()} {param_value}")
|
fcgi_app_lines.append(f" set-param {param_name.upper()} {param_value}")
|
||||||
|
|
||||||
|
# Pass headers
|
||||||
|
for header in self.pass_headers:
|
||||||
|
line = f" pass-header {header['name']}"
|
||||||
|
if header.get("condition"):
|
||||||
|
line += f" {header['condition']}"
|
||||||
|
fcgi_app_lines.append(line)
|
||||||
|
|
||||||
fcgi_app_definition = "\n".join(fcgi_app_lines)
|
fcgi_app_definition = "\n".join(fcgi_app_lines)
|
||||||
|
|
||||||
# Build metadata
|
# Build metadata
|
||||||
|
|
@ -143,7 +184,8 @@ class FastcgiPlugin(PluginInterface):
|
||||||
"document_root": self.document_root,
|
"document_root": self.document_root,
|
||||||
"index_file": self.index_file,
|
"index_file": self.index_file,
|
||||||
"path_info": self.path_info,
|
"path_info": self.path_info,
|
||||||
"custom_params_count": len(self.custom_params)
|
"custom_params_count": len(self.custom_params),
|
||||||
|
"pass_headers_count": len(self.pass_headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PluginResult(
|
return PluginResult(
|
||||||
|
|
|
||||||
|
|
@ -952,6 +952,7 @@ class TestFastcgiPlugin:
|
||||||
assert plugin.index_file == "index.php"
|
assert plugin.index_file == "index.php"
|
||||||
assert plugin.path_info is True
|
assert plugin.path_info is True
|
||||||
assert plugin.custom_params == {}
|
assert plugin.custom_params == {}
|
||||||
|
assert plugin.pass_headers == []
|
||||||
|
|
||||||
def test_fastcgi_plugin_configuration(self):
|
def test_fastcgi_plugin_configuration(self):
|
||||||
"""Test plugin configuration"""
|
"""Test plugin configuration"""
|
||||||
|
|
@ -1046,6 +1047,53 @@ class TestFastcgiPlugin:
|
||||||
|
|
||||||
assert result.haproxy_config is None or result.haproxy_config == ""
|
assert result.haproxy_config is None or result.haproxy_config == ""
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_pass_headers_string(self):
|
||||||
|
"""Test pass_headers parsed from comma-separated string"""
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({"pass_headers": "Authorization, Proxy-Authorization"})
|
||||||
|
|
||||||
|
assert plugin.pass_headers == [
|
||||||
|
{"name": "Authorization", "condition": None},
|
||||||
|
{"name": "Proxy-Authorization", "condition": None},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
result = plugin.process(context)
|
||||||
|
fcgi_app_def = result.global_configs[0]
|
||||||
|
|
||||||
|
assert "pass-header Authorization" in fcgi_app_def
|
||||||
|
assert "pass-header Proxy-Authorization" in fcgi_app_def
|
||||||
|
assert result.metadata["pass_headers_count"] == 2
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_pass_headers_list(self):
|
||||||
|
"""Test pass_headers parsed from list of strings and dicts with condition"""
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({
|
||||||
|
"pass_headers": [
|
||||||
|
"Authorization",
|
||||||
|
{"name": "X-Custom-Header", "condition": "if { ssl_fc }"},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert plugin.pass_headers == [
|
||||||
|
{"name": "Authorization", "condition": None},
|
||||||
|
{"name": "X-Custom-Header", "condition": "if { ssl_fc }"},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
result = plugin.process(context)
|
||||||
|
fcgi_app_def = result.global_configs[0]
|
||||||
|
|
||||||
|
assert "pass-header Authorization" in fcgi_app_def
|
||||||
|
assert "pass-header X-Custom-Header if { ssl_fc }" in fcgi_app_def
|
||||||
|
assert result.metadata["pass_headers_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
class TestPluginManager:
|
class TestPluginManager:
|
||||||
"""Test cases for PluginManager"""
|
"""Test cases for PluginManager"""
|
||||||
|
|
|
||||||
|
|
@ -920,7 +920,7 @@ class TestACME:
|
||||||
"""Test that Pebble successfully issues a certificate"""
|
"""Test that Pebble successfully issues a certificate"""
|
||||||
# Wait for certificate issuance (Certbot runs in background loop)
|
# Wait for certificate issuance (Certbot runs in background loop)
|
||||||
# Typical time: 10-15 seconds from container start
|
# Typical time: 10-15 seconds from container start
|
||||||
max_retries = 30
|
max_retries = 60
|
||||||
check_interval = 1
|
check_interval = 1
|
||||||
has_success = False
|
has_success = False
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue