|
@@ -0,0 +1,460 @@
|
|
|
|
|
+# Configuration
|
|
|
|
|
+
|
|
|
|
|
+Configuration objects for dummy backend servers and reverse proxies.
|
|
|
|
|
+
|
|
|
|
|
+## BackendConfig
|
|
|
|
|
+
|
|
|
|
|
+::: httphound.backend.BackendConfig
|
|
|
|
|
+ options:
|
|
|
|
|
+ show_root_heading: true
|
|
|
|
|
+ show_source: false
|
|
|
|
|
+
|
|
|
|
|
+### Overview
|
|
|
|
|
+
|
|
|
|
|
+`BackendConfig` defines the behavior of the dummy backend HTTP server used in tests.
|
|
|
|
|
+
|
|
|
|
|
+### Basic Usage
|
|
|
|
|
+```python
|
|
|
|
|
+from httphound.main import BackendConfig
|
|
|
|
|
+
|
|
|
|
|
+# Minimal configuration (all defaults)
|
|
|
|
|
+backend_config = BackendConfig()
|
|
|
|
|
+
|
|
|
|
|
+# Custom configuration
|
|
|
|
|
+backend_config = BackendConfig(
|
|
|
|
|
+ host="127.0.0.1",
|
|
|
|
|
+ port=9999,
|
|
|
|
|
+ response_status=200,
|
|
|
|
|
+ response_headers={"X-Custom": "value"},
|
|
|
|
|
+ response_body="Hello from backend"
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Parameters
|
|
|
|
|
+
|
|
|
|
|
+#### `host: str`
|
|
|
|
|
+IP address for backend to bind to.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"127.0.0.1"`
|
|
|
|
|
+```python
|
|
|
|
|
+BackendConfig(host="127.0.0.1") # Localhost only
|
|
|
|
|
+BackendConfig(host="0.0.0.0") # All interfaces
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+This is used also to populate HAProxy configuration file (when in **template** mode).
|
|
|
|
|
+
|
|
|
|
|
+#### `port: int`
|
|
|
|
|
+Port number for backend to listen on.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `9999`
|
|
|
|
|
+
|
|
|
|
|
+!!! warning "Type Matters"
|
|
|
|
|
+ Must be an integer, not a string!
|
|
|
|
|
+```python
|
|
|
|
|
+ BackendConfig(port=9999) # correct
|
|
|
|
|
+ BackendConfig(port="9999") # wrong!
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `response_status: int`
|
|
|
|
|
+HTTP status code to return when a request to the backend is made.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `200`
|
|
|
|
|
+```python
|
|
|
|
|
+BackendConfig(response_status=200) # OK
|
|
|
|
|
+BackendConfig(response_status=201) # Created
|
|
|
|
|
+BackendConfig(response_status=301) # Moved Permanently
|
|
|
|
|
+BackendConfig(response_status=404) # Not Found
|
|
|
|
|
+BackendConfig(response_status=500) # Internal Server Error
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `response_headers: Dict[str, str]`
|
|
|
|
|
+HTTP headers to include in response.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `{}` (empty dict)
|
|
|
|
|
+```python
|
|
|
|
|
+BackendConfig(response_headers={
|
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
|
+ "X-Custom-Header": "value123",
|
|
|
|
|
+ "Cache-Control": "no-cache"
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+!!! tip "Header Case"
|
|
|
|
|
+ Header names will be sent as specified. HTTP is case-insensitive for headers, but preserve the case you want. HAProxy usually lowercase these.
|
|
|
|
|
+
|
|
|
|
|
+#### `response_body: str`
|
|
|
|
|
+Response body content.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"OK"`
|
|
|
|
|
+```python
|
|
|
|
|
+BackendConfig(response_body="Hello World")
|
|
|
|
|
+BackendConfig(response_body='{"status": "ok"}')
|
|
|
|
|
+BackendConfig(response_body="<html><body>Test</body></html>")
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Examples
|
|
|
|
|
+
|
|
|
|
|
+#### Mock JSON API Backend
|
|
|
|
|
+```python
|
|
|
|
|
+backend_config = BackendConfig(
|
|
|
|
|
+ port=9999,
|
|
|
|
|
+ response_status=200,
|
|
|
|
|
+ response_headers={
|
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
|
+ "X-API-Version": "1.0"
|
|
|
|
|
+ },
|
|
|
|
|
+ response_body='{"status": "success", "data": [1, 2, 3]}'
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Error Response Backend
|
|
|
|
|
+```python
|
|
|
|
|
+backend_config = BackendConfig(
|
|
|
|
|
+ response_status=503,
|
|
|
|
|
+ response_headers={
|
|
|
|
|
+ "Content-Type": "text/plain",
|
|
|
|
|
+ "Retry-After": "300"
|
|
|
|
|
+ },
|
|
|
|
|
+ response_body="Service temporarily unavailable"
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Redirect Backend
|
|
|
|
|
+```python
|
|
|
|
|
+backend_config = BackendConfig(
|
|
|
|
|
+ response_status=301,
|
|
|
|
|
+ response_headers={
|
|
|
|
|
+ "Location": "https://example.com/new-location"
|
|
|
|
|
+ },
|
|
|
|
|
+ response_body=""
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## ProxyConfig
|
|
|
|
|
+
|
|
|
|
|
+::: httphound.proxy.ProxyConfig
|
|
|
|
|
+ options:
|
|
|
|
|
+ show_root_heading: true
|
|
|
|
|
+ show_source: false
|
|
|
|
|
+
|
|
|
|
|
+### Overview
|
|
|
|
|
+
|
|
|
|
|
+`ProxyConfig` defines how HAProxy is configured and started. Supports two modes:
|
|
|
|
|
+
|
|
|
|
|
+* **Template mode**: Generate config from Jinja2 template (default)
|
|
|
|
|
+* **Production mode**: Use existing HAProxy configuration files
|
|
|
|
|
+
|
|
|
|
|
+### Basic Usage
|
|
|
|
|
+
|
|
|
|
|
+#### Template Mode (Default)
|
|
|
|
|
+```python
|
|
|
|
|
+from httphound.main import ProxyConfig
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ binary_path=Path.home() / "bin/haproxy",
|
|
|
|
|
+ template_path="haproxy.cfg.tpl",
|
|
|
|
|
+ listen_port=4242
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Production Mode
|
|
|
|
|
+```python
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ binary_path="/usr/sbin/haproxy",
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ backend_name_to_patch="app_backend",
|
|
|
|
|
+ bind_address_override="*:4242"
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Common Parameters
|
|
|
|
|
+
|
|
|
|
|
+#### `binary_path: str`
|
|
|
|
|
+Path to HAProxy binary. Must exists on the filesystem and executable as the user who runs the tests.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"/usr/sbin/haproxy"`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(binary_path="/usr/sbin/haproxy") # Default path if HAProxy is installed as debian package
|
|
|
|
|
+ProxyConfig(binary_path=Path.home() / "bin/haproxy")
|
|
|
|
|
+ProxyConfig(binary_path="/usr/local/bin/haproxy")
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `working_dir: str`
|
|
|
|
|
+Temporary directory for generated configs.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"/tmp/httphound"`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(working_dir="/tmp/httphound")
|
|
|
|
|
+ProxyConfig(working_dir="/tmp/my-tests")
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `config_mode: str`
|
|
|
|
|
+Configuration mode: `"template"` or `"production"`.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"template"`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(config_mode="template") # Use Jinja2 template
|
|
|
|
|
+ProxyConfig(config_mode="production") # Use existing config
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `extra_args: List[str]`
|
|
|
|
|
+Additional command-line arguments for HAProxy.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `[]`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(extra_args=["-dM"]) # Memory debug mode
|
|
|
|
|
+ProxyConfig(extra_args=["-D"]) # Daemon mode
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Template Mode Parameters
|
|
|
|
|
+
|
|
|
|
|
+Used when `config_mode="template"` (default).
|
|
|
|
|
+
|
|
|
|
|
+#### `template_path: str`
|
|
|
|
|
+Path to Jinja2 template file.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"haproxy.cfg.tpl"`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(template_path="haproxy.cfg.tpl")
|
|
|
|
|
+ProxyConfig(template_path="templates/my-haproxy.tpl")
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `listen_addr: str`
|
|
|
|
|
+Address for HAProxy to bind to.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"*"` (all interfaces)
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(listen_addr="*") # All interfaces
|
|
|
|
|
+ProxyConfig(listen_addr="127.0.0.1") # Localhost only
|
|
|
|
|
+ProxyConfig(listen_addr="0.0.0.0") # Explicit all
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `listen_port: int`
|
|
|
|
|
+Port for HAProxy to listen on.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `4242`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(listen_port=4242) # Default
|
|
|
|
|
+ProxyConfig(listen_port=8080) # Custom
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `template_vars: Optional[Dict[str, List[str]]]`
|
|
|
|
|
+Additional variables to pass to template. Each key corresponds to the HAProxy template section where the directives must be added.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `{}`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(template_vars={
|
|
|
|
|
+ "max_connections": 1000,
|
|
|
|
|
+ "timeout_client": "30s",
|
|
|
|
|
+ "template_directives": {
|
|
|
|
|
+ "global": [
|
|
|
|
|
+ "maxconn 1000",
|
|
|
|
|
+ ],
|
|
|
|
|
+ "defaults": [
|
|
|
|
|
+ "retries 3",
|
|
|
|
|
+ ],
|
|
|
|
|
+ "frontend_http": [
|
|
|
|
|
+ "http-request deny deny_status 404 if path_beg -i /deny",
|
|
|
|
|
+ ],
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+These variables are rendered
|
|
|
|
|
+
|
|
|
|
|
+**In template:**
|
|
|
|
|
+```jinja
|
|
|
|
|
+global
|
|
|
|
|
+ ...
|
|
|
|
|
+ {%- if template_directives["global"] -%}
|
|
|
|
|
+ {{ for directive in template_directives["global"] }}
|
|
|
|
|
+ {{ directive }}
|
|
|
|
|
+ {%- endfor -%}
|
|
|
|
|
+ {%- endif -%}
|
|
|
|
|
+
|
|
|
|
|
+defaults
|
|
|
|
|
+ ...
|
|
|
|
|
+ {%- if template_directives["defaults"] -%}
|
|
|
|
|
+ {{ for directive in template_directives["defaults"] }}
|
|
|
|
|
+ {{ directive }}
|
|
|
|
|
+ {%- endfor -%}
|
|
|
|
|
+ {%- endif -%}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+frontend http
|
|
|
|
|
+ {%- if template_directives["frontend_http"] -%}
|
|
|
|
|
+ {{ for directive in template_directives["frontend_http"] }}
|
|
|
|
|
+ {{ directive }}
|
|
|
|
|
+ {%- endfor -%}
|
|
|
|
|
+ {%- endif -%}
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Production Mode Parameters
|
|
|
|
|
+
|
|
|
|
|
+Used when `config_mode="production"`.
|
|
|
|
|
+
|
|
|
|
|
+#### `production_config_path: str`
|
|
|
|
|
+Path to main HAProxy configuration file.
|
|
|
|
|
+
|
|
|
|
|
+**Required in production mode**
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg"
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `production_config_base_dir: str`
|
|
|
|
|
+Base directory containing config and includes (for multi-file configs).
|
|
|
|
|
+
|
|
|
|
|
+**Optional** - if set, entire directory tree is copied.
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ production_config_base_dir="/etc/haproxy/conf.d" # Copies whole directory
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `backend_name_to_patch: str`
|
|
|
|
|
+Name of backend section to patch with test backend address.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `"default_backend"`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ backend_name_to_patch="app_backend" # Must match config
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**In HAProxy config:**
|
|
|
|
|
+```haproxy
|
|
|
|
|
+backend app_backend
|
|
|
|
|
+ server app1 10.0.1.10:8080
|
|
|
|
|
+ server app2 10.0.1.11:8080
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**After patching:**
|
|
|
|
|
+```haproxy
|
|
|
|
|
+backend app_backend
|
|
|
|
|
+ server app1 127.0.0.1:9999 # Test backend
|
|
|
|
|
+ server app2 127.0.0.1:9999 # Test backend
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `bind_address_override: str`
|
|
|
|
|
+Override all bind addresses to avoid port conflicts.
|
|
|
|
|
+
|
|
|
|
|
+**Optional** - if not set, original bind addresses are used.
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ bind_address_override="*:4242" # Override all binds
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Original config:**
|
|
|
|
|
+```haproxy
|
|
|
|
|
+frontend web
|
|
|
|
|
+ bind *:80
|
|
|
|
|
+ bind *:443 ssl crt /path/to/cert
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**After override:**
|
|
|
|
|
+```haproxy
|
|
|
|
|
+frontend web
|
|
|
|
|
+ bind *:4242
|
|
|
|
|
+ bind *:4242 ssl crt /path/to/cert
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### `skip_backend_injection: bool`
|
|
|
|
|
+If True, don't patch backend server addresses.
|
|
|
|
|
+
|
|
|
|
|
+**Default:** `False`
|
|
|
|
|
+```python
|
|
|
|
|
+ProxyConfig(
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ skip_backend_injection=True # Don't modify backends
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Use this when:
|
|
|
|
|
+- Production config already points to test backend
|
|
|
|
|
+- Tests doesn't need to receive backend responses (test proxy configuration only)
|
|
|
|
|
+- You've manually edited the config
|
|
|
|
|
+- Backend patching is too complex
|
|
|
|
|
+
|
|
|
|
|
+### Examples
|
|
|
|
|
+
|
|
|
|
|
+#### Simple Template Mode
|
|
|
|
|
+```python
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ binary_path=Path.home() / "bin/haproxy",
|
|
|
|
|
+ config_mode="template",
|
|
|
|
|
+ template_path="haproxy.cfg.tpl",
|
|
|
|
|
+ listen_port=4242
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Template with custom directives
|
|
|
|
|
+```python
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ binary_path="/usr/sbin/haproxy",
|
|
|
|
|
+ template_path="templates/advanced.tpl",
|
|
|
|
|
+ listen_port=8080,
|
|
|
|
|
+ template_directives={
|
|
|
|
|
+ "frontend_http": [
|
|
|
|
|
+ "http-request deny deny_status 404 if path_beg -i /deny",
|
|
|
|
|
+ ],
|
|
|
|
|
+ },
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Production Mode - Single File
|
|
|
|
|
+```python
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ binary_path="/usr/sbin/haproxy",
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ backend_name_to_patch="app_backend",
|
|
|
|
|
+ bind_address_override="127.0.0.1:4242"
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Production Mode - Multi-File
|
|
|
|
|
+```python
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ binary_path="/usr/sbin/haproxy",
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/etc/haproxy/haproxy.cfg",
|
|
|
|
|
+ production_config_base_dir="/etc/haproxy", # Copy whole tree
|
|
|
|
|
+ backend_name_to_patch="web_backend",
|
|
|
|
|
+ bind_address_override="*:4242"
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Production Mode - Manual Backend
|
|
|
|
|
+```python
|
|
|
|
|
+# When production config already has correct backend address
|
|
|
|
|
+proxy_config = ProxyConfig(
|
|
|
|
|
+ config_mode="production",
|
|
|
|
|
+ production_config_path="/tmp/test-haproxy.cfg",
|
|
|
|
|
+ skip_backend_injection=True, # Don't patch
|
|
|
|
|
+ bind_address_override="*:4242"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+# Backend must match config
|
|
|
|
|
+backend_config = BackendConfig(
|
|
|
|
|
+ host="127.0.0.1",
|
|
|
|
|
+ port=8080 # Must match what's in config
|
|
|
|
|
+)
|
|
|
|
|
+```
|