Add log-stderr, keep-conn, and path_info presets to FastCGI plugin
This commit is contained in:
parent
3dfc570d57
commit
52558e9277
3 changed files with 143 additions and 5 deletions
|
|
@ -24,10 +24,28 @@ Automatically generates HAProxy `fcgi-app` configuration that defines required C
|
||||||
| `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` | PATH_INFO preset or custom regex | `php` |
|
||||||
|
| `log_stderr` | Forward FastCGI stderr to HAProxy logs | `false` |
|
||||||
|
| `keep_conn` | Reuse FastCGI connections between requests | `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` | HTTP headers to forward to the FastCGI app | (optional) |
|
||||||
|
|
||||||
|
### `path_info` — PATH_INFO Presets
|
||||||
|
|
||||||
|
The `path_info` option accepts a preset name or a custom regex. Built-in presets:
|
||||||
|
|
||||||
|
| Preset | Language | Regex |
|
||||||
|
|----------|----------------|---------------------------------|
|
||||||
|
| `php` | PHP (default) | `^(/.+\.php)(/.*)?$` |
|
||||||
|
| `python` | Python FastCGI | `^(/.+\.py)(/.*)?$` |
|
||||||
|
| `perl` | Perl/CGI | `^(/.+\.(pl\|cgi))(/.*)?$` |
|
||||||
|
| `ruby` | Ruby FastCGI | `^(/.+\.rb)(/.*)?$` |
|
||||||
|
| `any` | Generic | `^(.+?)(/.*)?$` |
|
||||||
|
|
||||||
|
Pass `false` to disable PATH_INFO entirely, or any other string to use it as a custom regex directly.
|
||||||
|
|
||||||
|
Setting `path_info: true` maps to `php` for backward compatibility.
|
||||||
|
|
||||||
### `pass_headers` — Forwarding HTTP Headers
|
### `pass_headers` — Forwarding HTTP Headers
|
||||||
|
|
||||||
:::important
|
:::important
|
||||||
|
|
@ -154,6 +172,7 @@ easymapping:
|
||||||
fcgi-app fcgi_phpapp_local
|
fcgi-app fcgi_phpapp_local
|
||||||
docroot /var/www/html
|
docroot /var/www/html
|
||||||
index index.php
|
index index.php
|
||||||
|
option keep-conn
|
||||||
path-info ^(/.+\.php)(/.*)?$
|
path-info ^(/.+\.php)(/.*)?$
|
||||||
pass-header Authorization
|
pass-header Authorization
|
||||||
pass-header Proxy-Authorization
|
pass-header Proxy-Authorization
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,23 @@ from plugins import PluginContext, PluginInterface, PluginResult, PluginType
|
||||||
class FastcgiPlugin(PluginInterface):
|
class FastcgiPlugin(PluginInterface):
|
||||||
"""Plugin to configure FastCGI parameters for PHP-FPM"""
|
"""Plugin to configure FastCGI parameters for PHP-FPM"""
|
||||||
|
|
||||||
|
# Built-in path-info regex presets
|
||||||
|
PATH_INFO_PRESETS = {
|
||||||
|
"php": r"^(/.+\.php)(/.*)?$",
|
||||||
|
"python": r"^(/.+\.py)(/.*)?$",
|
||||||
|
"perl": r"^(/.+\.(?:pl|cgi))(/.*)?$",
|
||||||
|
"ruby": r"^(/.+\.rb)(/.*)?$",
|
||||||
|
"any": r"^(.+?)(/.*)?$",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self.document_root = "/var/www/html"
|
self.document_root = "/var/www/html"
|
||||||
self.script_filename = "%[path]"
|
self.script_filename = "%[path]"
|
||||||
self.index_file = "index.php"
|
self.index_file = "index.php"
|
||||||
self.path_info = True
|
self.path_info = "php" # False | preset name | custom regex string
|
||||||
|
self.log_stderr = False
|
||||||
|
self.keep_conn = True
|
||||||
self.custom_params = {}
|
self.custom_params = {}
|
||||||
self.pass_headers = [] # list of {"name": str, "condition": str|None}
|
self.pass_headers = [] # list of {"name": str, "condition": str|None}
|
||||||
|
|
||||||
|
|
@ -80,6 +91,16 @@ 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
|
||||||
|
- path_info: Enable PATH_INFO splitting. Accepts:
|
||||||
|
- False / "false" / "no" / "0": disabled
|
||||||
|
- "php" (default): ^(/.+[.]php)(/.*)?$
|
||||||
|
- "python": ^(/.+[.]py)(/.*)?$
|
||||||
|
- "perl": ^(/.+[.](pl|cgi))(/.*)?$
|
||||||
|
- "ruby": ^(/.+[.]rb)(/.*)?$
|
||||||
|
- "any": ^(.+?)(/.*)?$
|
||||||
|
- any other string: used as-is as the regex
|
||||||
|
- log_stderr: Forward FastCGI app's stderr to HAProxy logs (default: false)
|
||||||
|
- keep_conn: Reuse FastCGI connections between requests (default: true)
|
||||||
- pass_headers: Headers to forward to the FastCGI application.
|
- pass_headers: Headers to forward to the FastCGI application.
|
||||||
HAProxy omits Authorization, Proxy-Authorization, and hop-by-hop headers
|
HAProxy omits Authorization, Proxy-Authorization, and hop-by-hop headers
|
||||||
by default — this directive is required to pass them through.
|
by default — this directive is required to pass them through.
|
||||||
|
|
@ -107,7 +128,19 @@ class FastcgiPlugin(PluginInterface):
|
||||||
self.index_file = config["index_file"]
|
self.index_file = config["index_file"]
|
||||||
|
|
||||||
if "path_info" in config:
|
if "path_info" in config:
|
||||||
self.path_info = str(config["path_info"]).lower() in ["true", "1", "yes"]
|
val = str(config["path_info"]).lower()
|
||||||
|
if val in ["false", "0", "no"]:
|
||||||
|
self.path_info = False
|
||||||
|
elif val in ["true", "1", "yes"]:
|
||||||
|
self.path_info = "php" # backward-compatible: true → php preset
|
||||||
|
else:
|
||||||
|
self.path_info = str(config["path_info"]) # preset name or custom regex
|
||||||
|
|
||||||
|
if "log_stderr" in config:
|
||||||
|
self.log_stderr = str(config["log_stderr"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "keep_conn" in config:
|
||||||
|
self.keep_conn = str(config["keep_conn"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
if "custom_params" in config:
|
if "custom_params" in config:
|
||||||
self.custom_params = config["custom_params"]
|
self.custom_params = config["custom_params"]
|
||||||
|
|
@ -155,9 +188,16 @@ class FastcgiPlugin(PluginInterface):
|
||||||
fcgi_app_lines.append(f" docroot {self.document_root}")
|
fcgi_app_lines.append(f" docroot {self.document_root}")
|
||||||
fcgi_app_lines.append(f" index {self.index_file}")
|
fcgi_app_lines.append(f" index {self.index_file}")
|
||||||
|
|
||||||
|
if self.log_stderr:
|
||||||
|
fcgi_app_lines.append(" log-stderr")
|
||||||
|
|
||||||
|
if self.keep_conn:
|
||||||
|
fcgi_app_lines.append(" option keep-conn")
|
||||||
|
|
||||||
# PATH_INFO support
|
# PATH_INFO support
|
||||||
if self.path_info:
|
if self.path_info:
|
||||||
fcgi_app_lines.append(" path-info ^(/.+\\.php)(/.*)?$")
|
regex = self.PATH_INFO_PRESETS.get(self.path_info, self.path_info)
|
||||||
|
fcgi_app_lines.append(f" path-info {regex}")
|
||||||
|
|
||||||
# Set SCRIPT_FILENAME if customized
|
# Set SCRIPT_FILENAME if customized
|
||||||
if self.script_filename and self.script_filename != "%[path]":
|
if self.script_filename and self.script_filename != "%[path]":
|
||||||
|
|
@ -184,6 +224,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,
|
||||||
|
"log_stderr": self.log_stderr,
|
||||||
|
"keep_conn": self.keep_conn,
|
||||||
"custom_params_count": len(self.custom_params),
|
"custom_params_count": len(self.custom_params),
|
||||||
"pass_headers_count": len(self.pass_headers)
|
"pass_headers_count": len(self.pass_headers)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -950,7 +950,9 @@ class TestFastcgiPlugin:
|
||||||
assert plugin.enabled is True
|
assert plugin.enabled is True
|
||||||
assert plugin.document_root == "/var/www/html"
|
assert plugin.document_root == "/var/www/html"
|
||||||
assert plugin.index_file == "index.php"
|
assert plugin.index_file == "index.php"
|
||||||
assert plugin.path_info is True
|
assert plugin.path_info == "php"
|
||||||
|
assert plugin.log_stderr is False
|
||||||
|
assert plugin.keep_conn is True
|
||||||
assert plugin.custom_params == {}
|
assert plugin.custom_params == {}
|
||||||
assert plugin.pass_headers == []
|
assert plugin.pass_headers == []
|
||||||
|
|
||||||
|
|
@ -1094,6 +1096,81 @@ class TestFastcgiPlugin:
|
||||||
assert "pass-header X-Custom-Header if { ssl_fc }" in fcgi_app_def
|
assert "pass-header X-Custom-Header if { ssl_fc }" in fcgi_app_def
|
||||||
assert result.metadata["pass_headers_count"] == 2
|
assert result.metadata["pass_headers_count"] == 2
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_log_stderr(self):
|
||||||
|
"""Test log-stderr directive is emitted when enabled"""
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({"log_stderr": "true"})
|
||||||
|
assert plugin.log_stderr is True
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert "log-stderr" in result.global_configs[0]
|
||||||
|
assert result.metadata["log_stderr"] is True
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_keep_conn_disabled(self):
|
||||||
|
"""Test option keep-conn is omitted when disabled"""
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({"keep_conn": "false"})
|
||||||
|
assert plugin.keep_conn is False
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert "option keep-conn" not in result.global_configs[0]
|
||||||
|
assert result.metadata["keep_conn"] is False
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_path_info_presets(self):
|
||||||
|
"""Test path_info presets resolve to correct regexes"""
|
||||||
|
presets = {
|
||||||
|
"php": r"^(/.+\.php)(/.*)?$",
|
||||||
|
"python": r"^(/.+\.py)(/.*)?$",
|
||||||
|
"perl": r"^(/.+\.(?:pl|cgi))(/.*)?$",
|
||||||
|
"ruby": r"^(/.+\.rb)(/.*)?$",
|
||||||
|
"any": r"^(.+?)(/.*)?$",
|
||||||
|
}
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
for preset, expected_regex in presets.items():
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({"path_info": preset})
|
||||||
|
assert plugin.path_info == preset
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert f"path-info {expected_regex}" in result.global_configs[0], \
|
||||||
|
f"Preset '{preset}' did not emit expected regex"
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_path_info_custom_regex(self):
|
||||||
|
"""Test path_info accepts a custom regex"""
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({"path_info": r"^(/.+\.fcgi)(/.*)?$"})
|
||||||
|
assert plugin.path_info == r"^(/.+\.fcgi)(/.*)?$"
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert r"path-info ^(/.+\.fcgi)(/.*)?$" in result.global_configs[0]
|
||||||
|
|
||||||
|
def test_fastcgi_plugin_path_info_backward_compat(self):
|
||||||
|
"""Test path_info: true maps to php preset (backward compatibility)"""
|
||||||
|
plugin = FastcgiPlugin()
|
||||||
|
plugin.configure({"path_info": "true"})
|
||||||
|
assert plugin.path_info == "php"
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={}, easymapping=[], container_env={},
|
||||||
|
domain="phpapp.local", port="80", host_config={}
|
||||||
|
)
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert r"path-info ^(/.+\.php)(/.*)?$" in result.global_configs[0]
|
||||||
|
|
||||||
|
|
||||||
class TestPluginManager:
|
class TestPluginManager:
|
||||||
"""Test cases for PluginManager"""
|
"""Test cases for PluginManager"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue