A production-quality pure-Python WSGI server with robust HTTP protocol support and comprehensive configuration options
—
Support for parsing trusted proxy headers (X-Forwarded-For, X-Forwarded-Proto, etc.) when running behind reverse proxies or load balancers.
WSGI middleware for handling proxy headers from trusted sources, modifying the WSGI environ to reflect the original client request information.
def proxy_headers_middleware(app, trusted_proxy=None, trusted_proxy_count=1, trusted_proxy_headers=None, clear_untrusted=True, log_untrusted=False, logger=logger):
"""
WSGI middleware for processing trusted proxy headers.
Parameters:
- app: WSGI application to wrap
- trusted_proxy (str): IP address of trusted proxy (e.g., '127.0.0.1')
- trusted_proxy_count (int): Number of trusted proxy hops to process
- trusted_proxy_headers (set): Set of header names to trust and process
- clear_untrusted (bool): Remove untrusted proxy headers (default: True)
- log_untrusted (bool): Log when untrusted headers are encountered (default: False)
- logger: Logger for warnings and errors (default: waitress logger)
Returns:
WSGI application: Wrapped application with proxy header processing
Notes:
- Modifies WSGI environ based on trusted proxy headers
- Updates REMOTE_ADDR, HTTP_HOST, wsgi.url_scheme as appropriate
- Only processes headers from configured trusted sources
"""Low-level function for parsing and validating proxy headers from trusted sources.
def parse_proxy_headers(environ, trusted_proxy_count, trusted_proxy_headers, logger=logger):
"""
Parse trusted proxy headers and update WSGI environ.
Parameters:
- environ (dict): WSGI environ dictionary to modify
- trusted_proxy_count (int): Number of proxy hops to trust
- trusted_proxy_headers (set): Headers to process
- logger: Logger for warnings and errors (default: waitress logger)
Returns:
set: Set of untrusted header keys that were processed
Raises:
MalformedProxyHeader: When proxy headers contain invalid data
"""Classes and constants for handling proxy header information.
class MalformedProxyHeader(Exception):
"""
Raised when proxy headers contain invalid or malformed data.
This can occur when:
- Forwarded headers have invalid syntax
- X-Forwarded-* headers contain non-IP addresses
- Header values exceed expected formats
"""
class Forwarded:
"""
NamedTuple for parsed RFC 7239 Forwarded header data.
Attributes:
- by (str): Proxy identifier that forwarded the request
- for_ (str): Client identifier (usually IP address)
- host (str): Host header value from original request
- proto (str): Protocol scheme (http/https) from original request
"""
by: str
for_: str
host: str
proto: str
PROXY_HEADERS = frozenset([
'X_FORWARDED_FOR',
'X_FORWARDED_HOST',
'X_FORWARDED_PROTO',
'X_FORWARDED_PORT',
'X_FORWARDED_BY',
'FORWARDED'
])Configure waitress to trust specific proxy sources and process their headers.
from waitress import serve
# Trust single reverse proxy (e.g., nginx on same server)
serve(app,
trusted_proxy='127.0.0.1',
trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'},
host='127.0.0.1',
port=8080
)
# This configuration will:
# - Only trust headers from 127.0.0.1
# - Process X-Forwarded-For and X-Forwarded-Proto headers
# - Ignore proxy headers from other sources# Trust multiple proxy hops (e.g., Cloudflare + nginx)
serve(app,
trusted_proxy_count=2, # Trust 2 levels of proxies
trusted_proxy_headers={
'x-forwarded-for',
'x-forwarded-proto',
'x-forwarded-host',
'x-real-ip'
}
)
# This processes headers through 2 proxy layers:
# Client -> Cloudflare -> nginx -> waitress# Support all common proxy headers
serve(app,
trusted_proxy='10.0.0.1', # Load balancer IP
trusted_proxy_headers={
'x-forwarded-for', # Client IP chain
'x-forwarded-proto', # Original protocol (http/https)
'x-forwarded-host', # Original Host header
'x-forwarded-port', # Original port
'x-real-ip', # Direct client IP (nginx)
'forwarded' # RFC 7239 standard header
},
clear_untrusted=True, # Remove headers from untrusted sources
log_untrusted=True # Log untrusted header attempts
)Use proxy headers middleware for fine-grained control in WSGI applications.
from waitress.proxy_headers import proxy_headers_middleware
from waitress import serve
def my_app(environ, start_response):
# Application sees corrected environ values
client_ip = environ['REMOTE_ADDR'] # Real client IP
original_host = environ['HTTP_HOST'] # Original host header
scheme = environ['wsgi.url_scheme'] # Original scheme (http/https)
status = '200 OK'
headers = [('Content-Type', 'text/plain')]
start_response(status, headers)
response = f"Client IP: {client_ip}\nHost: {original_host}\nScheme: {scheme}"
return [response.encode('utf-8')]
# Wrap application with proxy middleware
proxied_app = proxy_headers_middleware(
my_app,
trusted_proxy='192.168.1.100',
trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'}
)
# Serve wrapped application
serve(proxied_app, host='127.0.0.1', port=8080)Understanding how different proxy headers are processed.
# Original request through proxy chain:
# Client (203.0.113.1) -> Proxy1 (10.0.0.1) -> Proxy2 (127.0.0.1) -> Waitress
# X-Forwarded-For: 203.0.113.1, 10.0.0.1
# With trusted_proxy_count=2:
environ_before = {
'REMOTE_ADDR': '127.0.0.1', # Direct connection from Proxy2
'HTTP_X_FORWARDED_FOR': '203.0.113.1, 10.0.0.1'
}
environ_after = {
'REMOTE_ADDR': '203.0.113.1', # Original client IP
'HTTP_X_FORWARDED_FOR': '203.0.113.1, 10.0.0.1' # Preserved
}# HTTPS request through HTTP proxy:
# Client (HTTPS) -> nginx (HTTP) -> Waitress
# X-Forwarded-Proto: https
# With trusted proxy headers:
environ_before = {
'wsgi.url_scheme': 'http', # Direct HTTP connection
'HTTP_X_FORWARDED_PROTO': 'https'
}
environ_after = {
'wsgi.url_scheme': 'https', # Original client scheme
'HTTP_X_FORWARDED_PROTO': 'https' # Preserved
}# Standard Forwarded header processing:
# Forwarded: for=203.0.113.1;proto=https;host=example.com
environ_before = {
'REMOTE_ADDR': '127.0.0.1',
'HTTP_HOST': 'localhost:8080',
'wsgi.url_scheme': 'http',
'HTTP_FORWARDED': 'for=203.0.113.1;proto=https;host=example.com'
}
environ_after = {
'REMOTE_ADDR': '203.0.113.1', # From for= parameter
'HTTP_HOST': 'example.com', # From host= parameter
'wsgi.url_scheme': 'https', # From proto= parameter
'HTTP_FORWARDED': '...' # Preserved
}Important security aspects when handling proxy headers.
# Security best practices:
TRUST_CONFIGURATION = "Only trust known proxy IPs"
HEADER_VALIDATION = "Validate all header values"
CLEAR_UNTRUSTED = "Remove headers from untrusted sources"
LOG_ATTEMPTS = "Log untrusted header attempts"
# Common security issues:
HEADER_SPOOFING = "Clients can set X-Forwarded-* headers"
IP_SPOOFING = "Untrusted proxies can forge client IPs"
PROTOCOL_CONFUSION = "HTTP/HTTPS confusion without validation"
# Mitigation strategies:
WHITELIST_PROXIES = "trusted_proxy configuration"
VALIDATE_HEADERS = "Automatic validation in parse_proxy_headers"
REMOVE_UNTRUSTED = "clear_untrusted=True (default)"
MONITOR_ATTEMPTS = "log_untrusted=True for monitoring"# Production security configuration
serve(app,
# Only trust specific load balancer
trusted_proxy='10.0.1.100',
# Limit to essential headers
trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'},
# Security defaults
clear_untrusted=True, # Remove untrusted headers
log_untrusted=True, # Log security attempts
# Bind only to internal interface
host='127.0.0.1',
port=8080
)
# High-security configuration with validation
def validate_proxy_headers(app):
def wrapper(environ, start_response):
# Additional header validation
xff = environ.get('HTTP_X_FORWARDED_FOR', '')
if xff and not is_valid_ip_list(xff):
# Reject request with invalid IPs
start_response('400 Bad Request', [])
return [b'Invalid proxy headers']
return app(environ, start_response)
return wrapper
secure_app = validate_proxy_headers(
proxy_headers_middleware(app, trusted_proxy='192.168.1.1')
)Common deployment patterns with proxy header handling.
# nginx configuration
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Host $host;
}
}# Corresponding waitress configuration
serve(app,
trusted_proxy='127.0.0.1',
trusted_proxy_headers={
'x-forwarded-for',
'x-forwarded-proto',
'x-forwarded-host'
},
host='127.0.0.1',
port=8080
)# HAProxy configuration
frontend web_frontend
bind *:80
option forwardfor
http-request set-header X-Forwarded-Proto http
default_backend web_servers
backend web_servers
server web1 127.0.0.1:8080# Waitress configuration for HAProxy
serve(app,
trusted_proxy='127.0.0.1',
trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'},
host='127.0.0.1',
port=8080
)Install with Tessl CLI
npx tessl i tessl/pypi-waitress