summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <bertjw@regeer.org>2019-07-24 21:06:34 -0600
committerGitHub <noreply@github.com>2019-07-24 21:06:34 -0600
commit94e23114bf4e8db9507f3550294037a4804eb053 (patch)
tree9048993d211447e6df25d4cfaacd48a370cec47e
parente2210c9258702b7a46fa23f3f5e8389d56999748 (diff)
parent0667b8eee7ce33af659f5e6e09da3b31a7f78cfd (diff)
downloadwaitress-94e23114bf4e8db9507f3550294037a4804eb053.tar.gz
Merge pull request #259 from Pylons/proxy-header-middleware
move proxy headers to a middleware and return 400 for malformed values
-rw-r--r--.coveragerc14
-rw-r--r--.gitignore1
-rw-r--r--setup.cfg1
-rw-r--r--tox.ini9
-rw-r--r--waitress/adjustments.py7
-rw-r--r--waitress/proxy_headers.py351
-rw-r--r--waitress/server.py16
-rw-r--r--waitress/task.py294
-rw-r--r--waitress/tests/fixtureapps/echo.py54
-rw-r--r--waitress/tests/test_functional.py97
-rw-r--r--waitress/tests/test_proxy_headers.py695
-rw-r--r--waitress/tests/test_task.py617
-rw-r--r--waitress/utilities.py139
13 files changed, 1295 insertions, 1000 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..cbacd63
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,14 @@
+[run]
+parallel = true
+concurrency =
+ thread
+ multiprocessing
+source =
+ waitress
+
+omit =
+ waitress/tests/fixtureapps/getline.py
+
+[report]
+show_missing = true
+precision = 2
diff --git a/.gitignore b/.gitignore
index e1dbc2a..3a33b6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.pyc
env*/
.coverage
+.coverage.*
.idea/
.tox/
nosetests.xml
diff --git a/setup.cfg b/setup.cfg
index 4be5f9c..81cfbb1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,7 +3,6 @@ zip_ok = false
[nosetests]
match=^test
-where=waitress
nocapture=1
cover-package=waitress
cover-erase=1
diff --git a/tox.ini b/tox.ini
index 2533fb8..61cc9e9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
- nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
+ nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:waitress}
extras =
testing
@@ -21,7 +21,7 @@ extras =
[py-cover]
commands =
- coverage run --source=waitress --parallel-mode {envbindir}/nosetests
+ coverage run {envbindir}/nosetests waitress
coverage combine
coverage xml -o {envname}.xml
@@ -31,22 +31,23 @@ extras =
[testenv:py27-cover]
commands =
{[py-cover]commands}
+
setenv =
COVERAGE_FILE=.coverage.py2
[testenv:py35-cover]
commands =
{[py-cover]commands}
+
setenv =
COVERAGE_FILE=.coverage.py3
[testenv:coverage]
basepython = python3.5
commands =
- coverage erase
coverage combine
coverage xml
- coverage report --show-missing --fail-under=100 --omit=waitress/tests/fixtureapps/getline.py
+ coverage report --show-missing --fail-under=100
deps =
coverage
setenv =
diff --git a/waitress/adjustments.py b/waitress/adjustments.py
index 5c1879b..f33c570 100644
--- a/waitress/adjustments.py
+++ b/waitress/adjustments.py
@@ -17,7 +17,7 @@ import getopt
import socket
import warnings
-from .utilities import PROXY_HEADERS
+from .proxy_headers import PROXY_HEADERS
from .compat import (
PY2,
WIN,
@@ -27,7 +27,10 @@ from .compat import (
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
-KNOWN_PROXY_HEADERS = {header.lower().replace('_', '-') for header in PROXY_HEADERS}
+KNOWN_PROXY_HEADERS = frozenset(
+ header.lower().replace('_', '-')
+ for header in PROXY_HEADERS
+)
def asbool(s):
""" Return the boolean value ``True`` if the case-lowered value of string
diff --git a/waitress/proxy_headers.py b/waitress/proxy_headers.py
new file mode 100644
index 0000000..132fea8
--- /dev/null
+++ b/waitress/proxy_headers.py
@@ -0,0 +1,351 @@
+from collections import namedtuple
+
+from .utilities import logger, undquote, BadRequest
+
+
+PROXY_HEADERS = frozenset({
+ 'X_FORWARDED_FOR',
+ 'X_FORWARDED_HOST',
+ 'X_FORWARDED_PROTO',
+ 'X_FORWARDED_PORT',
+ 'X_FORWARDED_BY',
+ 'FORWARDED',
+})
+
+Forwarded = namedtuple('Forwarded', ['by', 'for_', 'host', 'proto'])
+
+
+class MalformedProxyHeader(Exception):
+ def __init__(self, header, reason, value):
+ self.header = header
+ self.reason = reason
+ self.value = value
+ super(MalformedProxyHeader, self).__init__(header, reason, value)
+
+
+def proxy_headers_middleware(
+ app,
+ trusted_proxy=None,
+ trusted_proxy_count=1,
+ trusted_proxy_headers=None,
+ clear_untrusted=True,
+ log_untrusted=False,
+ logger=logger,
+):
+ def translate_proxy_headers(environ, start_response):
+ untrusted_headers = PROXY_HEADERS
+ remote_peer = environ['REMOTE_ADDR']
+ if trusted_proxy == '*' or remote_peer == trusted_proxy:
+ try:
+ untrusted_headers = parse_proxy_headers(
+ environ,
+ trusted_proxy_count=trusted_proxy_count,
+ trusted_proxy_headers=trusted_proxy_headers,
+ logger=logger,
+ )
+ except MalformedProxyHeader as ex:
+ logger.warning(
+ 'Malformed proxy header "%s" from "%s": %s value: %s',
+ ex.header, remote_peer, ex.reason, ex.value)
+ error = BadRequest('Header "{0}" malformed.'.format(ex.header))
+ return error.wsgi_response(environ, start_response)
+
+ # Clear out the untrusted proxy headers
+ if clear_untrusted:
+ clear_untrusted_headers(
+ environ,
+ untrusted_headers,
+ log_warning=log_untrusted,
+ logger=logger,
+ )
+
+ return app(environ, start_response)
+ return translate_proxy_headers
+
+
+def parse_proxy_headers(
+ environ,
+ trusted_proxy_count,
+ trusted_proxy_headers,
+ logger=logger,
+):
+ if trusted_proxy_headers is None:
+ trusted_proxy_headers = set()
+
+ forwarded_for = []
+ forwarded_host = forwarded_proto = forwarded_port = forwarded = ""
+ client_addr = None
+ untrusted_headers = set(PROXY_HEADERS)
+
+ def raise_for_multiple_values():
+ raise ValueError(
+ 'Unspecified behavior for multiple values found in header',
+ )
+
+ if (
+ "x-forwarded-for" in trusted_proxy_headers
+ and "HTTP_X_FORWARDED_FOR" in environ
+ ):
+ try:
+ forwarded_for = []
+
+ for forward_hop in environ["HTTP_X_FORWARDED_FOR"].split(","):
+ forward_hop = forward_hop.strip()
+ forward_hop = undquote(forward_hop)
+
+ # Make sure that all IPv6 addresses are surrounded by brackets,
+ # this is assuming that the IPv6 representation here does not
+ # include a port number.
+
+ if "." not in forward_hop and (
+ ":" in forward_hop and forward_hop[-1] != "]"
+ ):
+ forwarded_for.append("[{}]".format(forward_hop))
+ else:
+ forwarded_for.append(forward_hop)
+
+ forwarded_for = forwarded_for[-trusted_proxy_count:]
+ client_addr = forwarded_for[0]
+
+ untrusted_headers.remove("X_FORWARDED_FOR")
+ except Exception as ex:
+ raise MalformedProxyHeader(
+ "X-Forwarded-For",
+ str(ex),
+ environ['HTTP_X_FORWARDED_FOR'],
+ )
+
+ if (
+ "x-forwarded-host" in trusted_proxy_headers
+ and "HTTP_X_FORWARDED_HOST" in environ
+ ):
+ try:
+ forwarded_host_multiple = []
+
+ for forward_host in environ["HTTP_X_FORWARDED_HOST"].split(","):
+ forward_host = forward_host.strip()
+ forward_host = undquote(forward_host)
+ forwarded_host_multiple.append(forward_host)
+
+ forwarded_host_multiple = forwarded_host_multiple[-trusted_proxy_count:]
+ forwarded_host = forwarded_host_multiple[0]
+
+ untrusted_headers.remove("X_FORWARDED_HOST")
+ except Exception as ex:
+ raise MalformedProxyHeader(
+ "X-Forwarded-Host",
+ str(ex),
+ environ['HTTP_X_FORWARDED_HOST'],
+ )
+
+ if "x-forwarded-proto" in trusted_proxy_headers:
+ try:
+ forwarded_proto = undquote(environ.get("HTTP_X_FORWARDED_PROTO", ""))
+ if ',' in forwarded_proto:
+ raise_for_multiple_values()
+ untrusted_headers.remove("X_FORWARDED_PROTO")
+ except Exception as ex:
+ raise MalformedProxyHeader(
+ "X-Forwarded-Proto",
+ str(ex),
+ environ['HTTP_X_FORWARDED_PROTO'],
+ )
+
+ if "x-forwarded-port" in trusted_proxy_headers:
+ try:
+ forwarded_port = undquote(environ.get("HTTP_X_FORWARDED_PORT", ""))
+ if ',' in forwarded_port:
+ raise_for_multiple_values()
+ untrusted_headers.remove("X_FORWARDED_PORT")
+ except Exception as ex:
+ raise MalformedProxyHeader(
+ "X-Forwarded-Port",
+ str(ex),
+ environ['HTTP_X_FORWARDED_PORT'],
+ )
+
+ if "x-forwarded-by" in trusted_proxy_headers:
+ # Waitress itself does not use X-Forwarded-By, but we can not
+ # remove it so it can get set in the environ
+ untrusted_headers.remove("X_FORWARDED_BY")
+
+ if "forwarded" in trusted_proxy_headers:
+ forwarded = environ.get("HTTP_FORWARDED", None)
+ untrusted_headers = PROXY_HEADERS - {"FORWARDED"}
+
+ # If the Forwarded header exists, it gets priority
+ if forwarded:
+ proxies = []
+ try:
+ for forwarded_element in forwarded.split(","):
+ # Remove whitespace that may have been introduced when
+ # appending a new entry
+ forwarded_element = forwarded_element.strip()
+
+ forwarded_for = forwarded_host = forwarded_proto = ""
+ forwarded_port = forwarded_by = ""
+
+ for pair in forwarded_element.split(";"):
+ pair = pair.lower()
+
+ if not pair:
+ continue
+
+ token, equals, value = pair.partition("=")
+
+ if equals != "=":
+ raise ValueError('Invalid forwarded-pair missing "="')
+
+ if token.strip() != token:
+ raise ValueError('Token may not be surrounded by whitespace')
+
+ if value.strip() != value:
+ raise ValueError('Value may not be surrounded by whitespace')
+
+ if token == "by":
+ forwarded_by = undquote(value)
+
+ elif token == "for":
+ forwarded_for = undquote(value)
+
+ elif token == "host":
+ forwarded_host = undquote(value)
+
+ elif token == "proto":
+ forwarded_proto = undquote(value)
+
+ else:
+ logger.warning("Unknown Forwarded token: %s" % token)
+
+ proxies.append(
+ Forwarded(
+ forwarded_by, forwarded_for, forwarded_host, forwarded_proto
+ )
+ )
+ except Exception as ex:
+ raise MalformedProxyHeader(
+ "Forwarded", str(ex), environ['HTTP_FORWARDED'],
+ )
+
+ proxies = proxies[-trusted_proxy_count:]
+
+ # Iterate backwards and fill in some values, the oldest entry that
+ # contains the information we expect is the one we use. We expect
+ # that intermediate proxies may re-write the host header or proto,
+ # but the oldest entry is the one that contains the information the
+ # client expects when generating URL's
+ #
+ # Forwarded: for="[2001:db8::1]";host="example.com:8443";proto="https"
+ # Forwarded: for=192.0.2.1;host="example.internal:8080"
+ #
+ # (After HTTPS header folding) should mean that we use as values:
+ #
+ # Host: example.com
+ # Protocol: https
+ # Port: 8443
+
+ for proxy in proxies[::-1]:
+ client_addr = proxy.for_ or client_addr
+ forwarded_host = proxy.host or forwarded_host
+ forwarded_proto = proxy.proto or forwarded_proto
+
+ if forwarded_proto:
+ forwarded_proto = forwarded_proto.lower()
+
+ if forwarded_proto not in {"http", "https"}:
+ raise MalformedProxyHeader(
+ "Forwarded Proto=" if forwarded else "X-Forwarded-Proto",
+ "unsupported proto value",
+ forwarded_proto,
+ )
+
+ # Set the URL scheme to the proxy provided proto
+ environ["wsgi.url_scheme"] = forwarded_proto
+
+ if not forwarded_port:
+ if forwarded_proto == "http":
+ forwarded_port = "80"
+
+ if forwarded_proto == "https":
+ forwarded_port = "443"
+
+ if forwarded_host:
+ if ":" in forwarded_host and forwarded_host[-1] != "]":
+ host, port = forwarded_host.rsplit(":", 1)
+ host, port = host.strip(), str(port)
+
+ # We trust the port in the Forwarded Host/X-Forwarded-Host over
+ # X-Forwarded-Port, or whatever we got from Forwarded
+ # Proto/X-Forwarded-Proto.
+
+ if forwarded_port != port:
+ forwarded_port = port
+
+ # We trust the proxy server's forwarded Host
+ environ["SERVER_NAME"] = host
+ environ["HTTP_HOST"] = forwarded_host
+ else:
+ # We trust the proxy server's forwarded Host
+ environ["SERVER_NAME"] = forwarded_host
+ environ["HTTP_HOST"] = forwarded_host
+
+ if forwarded_port:
+ if forwarded_port not in {"443", "80"}:
+ environ["HTTP_HOST"] = "{}:{}".format(
+ forwarded_host, forwarded_port
+ )
+ elif (
+ forwarded_port == "80"
+ and environ["wsgi.url_scheme"] != "http"
+ ):
+ environ["HTTP_HOST"] = "{}:{}".format(
+ forwarded_host, forwarded_port
+ )
+ elif (
+ forwarded_port == "443"
+ and environ["wsgi.url_scheme"] != "https"
+ ):
+ environ["HTTP_HOST"] = "{}:{}".format(
+ forwarded_host, forwarded_port
+ )
+
+ if forwarded_port:
+ environ["SERVER_PORT"] = str(forwarded_port)
+
+ if client_addr:
+ if ":" in client_addr and client_addr[-1] != "]":
+ addr, port = client_addr.rsplit(":", 1)
+ environ["REMOTE_ADDR"] = strip_brackets(addr.strip())
+ environ["REMOTE_PORT"] = port.strip()
+ else:
+ environ["REMOTE_ADDR"] = strip_brackets(client_addr.strip())
+ environ["REMOTE_HOST"] = environ["REMOTE_ADDR"]
+
+ return untrusted_headers
+
+
+def strip_brackets(addr):
+ if addr[0] == "[" and addr[-1] == "]":
+ return addr[1:-1]
+ return addr
+
+
+def clear_untrusted_headers(
+ environ, untrusted_headers, log_warning=False, logger=logger
+):
+ untrusted_headers_removed = [
+ header
+ for header in untrusted_headers
+ if environ.pop('HTTP_' + header, False) is not False
+ ]
+
+ if log_warning and untrusted_headers_removed:
+ untrusted_headers_removed = [
+ "-".join(x.capitalize() for x in header.split("_"))
+ for header in untrusted_headers_removed
+ ]
+ logger.warning(
+ "Removed untrusted headers (%s). Waitress recommends these be "
+ "removed upstream.",
+ ", ".join(untrusted_headers_removed),
+ )
diff --git a/waitress/server.py b/waitress/server.py
index 7ef930e..307c377 100644
--- a/waitress/server.py
+++ b/waitress/server.py
@@ -28,6 +28,7 @@ from waitress.compat import (
IPV6_V6ONLY,
)
from . import wasyncore
+from .proxy_headers import proxy_headers_middleware
def create_server(application,
map=None,
@@ -182,6 +183,21 @@ class BaseWSGIServer(wasyncore.dispatcher, object):
):
if adj is None:
adj = Adjustments(**kw)
+
+ if adj.trusted_proxy or adj.clear_untrusted_proxy_headers:
+ # wrap the application to deal with proxy headers
+ # we wrap it here because webtest subclasses the TcpWSGIServer
+ # directly and thus doesn't run any code that's in create_server
+ application = proxy_headers_middleware(
+ application,
+ trusted_proxy=adj.trusted_proxy,
+ trusted_proxy_count=adj.trusted_proxy_count,
+ trusted_proxy_headers=adj.trusted_proxy_headers,
+ clear_untrusted=adj.clear_untrusted_proxy_headers,
+ log_untrusted=adj.log_untrusted_proxy_headers,
+ logger=self.logger,
+ )
+
if map is None:
# use a nonglobal socket map by default to hopefully prevent
# conflicts with apps and libs that use the wasyncore global socket
diff --git a/waitress/task.py b/waitress/task.py
index 81e512f..78d8b83 100644
--- a/waitress/task.py
+++ b/waitress/task.py
@@ -21,13 +21,9 @@ import time
from .buffers import ReadOnlyFileBasedBuffer
from .compat import reraise, tobytes
from .utilities import (
- Forwarded,
- PROXY_HEADERS,
build_http_date,
- clear_untrusted_headers,
logger,
queue_logger,
- undquote,
)
rename_headers = { # or keep them without the HTTP_ prefix added
@@ -357,14 +353,9 @@ class ErrorTask(Task):
def execute(self):
e = self.request.error
- body = '%s\r\n\r\n%s' % (e.reason, e.body)
- tag = '\r\n\r\n(generated by waitress)'
- body = body + tag
- self.status = '%s %s' % (e.code, e.reason)
- cl = len(body)
- self.content_length = cl
- self.response_headers.append(('Content-Length', str(cl)))
- self.response_headers.append(('Content-Type', 'text/plain'))
+ status, headers, body = e.to_response()
+ self.status = status
+ self.response_headers.extend(headers)
if self.version == '1.1':
connection = self.request.headers.get('CONNECTION', '').lower()
if connection == 'close':
@@ -374,15 +365,17 @@ class ErrorTask(Task):
# HTTP 1.0
self.response_headers.append(('Connection', 'close'))
self.close_on_finish = True
+ self.content_length = len(body)
self.write(tobytes(body))
+
class WSGITask(Task):
"""A WSGI task produces a response from a WSGI application.
"""
environ = None
def execute(self):
- env = self.get_environment()
+ environ = self.get_environment()
def start_response(status, headers, exc_info=None):
if self.complete and not exc_info:
@@ -444,7 +437,7 @@ class WSGITask(Task):
return self.write
# Call the application to handle the request and write a response
- app_iter = self.channel.server.application(env, start_response)
+ app_iter = self.channel.server.application(environ, start_response)
can_close_app_iter = True
try:
@@ -501,236 +494,6 @@ class WSGITask(Task):
if can_close_app_iter and hasattr(app_iter, 'close'):
app_iter.close()
- def parse_proxy_headers(
- self,
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers=None,
- ):
- if trusted_proxy_headers is None:
- trusted_proxy_headers = set()
-
- forwarded_for = []
- forwarded_host = forwarded_proto = forwarded_port = forwarded = ""
- client_addr = None
- untrusted_headers = set(PROXY_HEADERS)
-
- def warn_unspecified_behavior(header):
- self.logger.warning(
- "Found multiple values in %s, this has unspecified behaviour. "
- "Ignoring header value.",
- header,
- )
-
- if "x-forwarded-for" in trusted_proxy_headers and "X_FORWARDED_FOR" in headers:
- forwarded_for = []
-
- for forward_hop in headers["X_FORWARDED_FOR"].split(","):
- forward_hop = forward_hop.strip()
- forward_hop = undquote(forward_hop)
-
- # Make sure that all IPv6 addresses are surrounded by brackets,
- # this is assuming that the IPv6 representation here does not
- # include a port number.
-
- if "." not in forward_hop and (
- ":" in forward_hop and forward_hop[-1] != "]"
- ):
- forwarded_for.append("[{}]".format(forward_hop))
- else:
- forwarded_for.append(forward_hop)
-
- forwarded_for = forwarded_for[-trusted_proxy_count:]
- client_addr = forwarded_for[0]
-
- untrusted_headers.remove("X_FORWARDED_FOR")
-
- if "x-forwarded-host" in trusted_proxy_headers and "X_FORWARDED_HOST" in headers:
- forwarded_host_multiple = []
-
- for forward_host in headers["X_FORWARDED_HOST"].split(","):
- forward_host = forward_host.strip()
- forward_host = undquote(forward_host)
- forwarded_host_multiple.append(forward_host)
-
- forwarded_host_multiple = forwarded_host_multiple[-trusted_proxy_count:]
- forwarded_host = forwarded_host_multiple[0]
-
- untrusted_headers.remove("X_FORWARDED_HOST")
-
- if "x-forwarded-proto" in trusted_proxy_headers:
- forwarded_proto = undquote(headers.get("X_FORWARDED_PROTO", ""))
- untrusted_headers.remove("X_FORWARDED_PROTO")
-
- if "," in forwarded_proto:
- forwarded_proto = ""
- warn_unspecified_behavior("X-Forwarded-Proto")
-
- if "x-forwarded-port" in trusted_proxy_headers:
- forwarded_port = undquote(headers.get("X_FORWARDED_PORT", ""))
- untrusted_headers.remove("X_FORWARDED_PORT")
-
- if "," in forwarded_port:
- forwarded_port = ""
- warn_unspecified_behavior("X-Forwarded-Port")
-
- if "x-forwarded-by" in trusted_proxy_headers:
- # Waitress itself does not use X-Forwarded-By, but we can not
- # remove it so it can get set in the environ
- untrusted_headers.remove("X_FORWARDED_BY")
-
- if "forwarded" in trusted_proxy_headers:
- forwarded = headers.get("FORWARDED", None)
- untrusted_headers = PROXY_HEADERS - {"FORWARDED"}
-
- # If the Forwarded header exists, it gets priority
- if forwarded:
- proxies = []
-
- for forwarded_element in forwarded.split(","):
- # Remove whitespace that may have been introduced when
- # appending a new entry
- forwarded_element = forwarded_element.strip()
-
- forwarded_for = forwarded_host = forwarded_proto = ""
- forwarded_port = forwarded_by = ""
-
- for pair in forwarded_element.split(";"):
- pair = pair.lower()
-
- if not pair:
- continue
-
- token, equals, value = pair.partition("=")
-
- if equals != "=":
- raise ValueError("Invalid forwarded-pair in Forwarded element")
-
- if token.strip() != token:
- raise ValueError("token may not be surrounded by whitespace")
-
- if value.strip() != value:
- raise ValueError("value may not be surrounded by whitespace")
-
- if token == "by":
- forwarded_by = undquote(value)
-
- elif token == "for":
- forwarded_for = undquote(value)
-
- elif token == "host":
- forwarded_host = undquote(value)
-
- elif token == "proto":
- forwarded_proto = undquote(value)
-
- else:
- self.logger.warning("Unknown Forwarded token: %s" % token)
-
- proxies.append(
- Forwarded(
- forwarded_by, forwarded_for, forwarded_host, forwarded_proto
- )
- )
-
- proxies = proxies[-trusted_proxy_count:]
-
- # Iterate backwards and fill in some values, the oldest entry that
- # contains the information we expect is the one we use. We expect
- # that intermediate proxies may re-write the host header or proto,
- # but the oldest entry is the one that contains the information the
- # client expects when generating URL's
- #
- # Forwarded: for="[2001:db8::1]";host="example.com:8443";proto="https"
- # Forwarded: for=192.0.2.1;host="example.internal:8080"
- #
- # (After HTTPS header folding) should mean that we use as values:
- #
- # Host: example.com
- # Protocol: https
- # Port: 8443
-
- for proxy in proxies[::-1]:
- client_addr = proxy.for_ or client_addr
- forwarded_host = proxy.host or forwarded_host
- forwarded_proto = proxy.proto or forwarded_proto
-
- if forwarded_proto:
- forwarded_proto = forwarded_proto.lower()
-
- if forwarded_proto not in {"http", "https"}:
- raise ValueError(
- 'Invalid "Forwarded Proto=" or "X-Forwarded-Proto" value.'
- )
-
- # Set the URL scheme to the proxy provided proto
- environ["wsgi.url_scheme"] = forwarded_proto
-
- if not forwarded_port:
- if forwarded_proto == "http":
- forwarded_port = "80"
-
- if forwarded_proto == "https":
- forwarded_port = "443"
-
- if forwarded_host:
- if ":" in forwarded_host and forwarded_host[-1] != "]":
- host, port = forwarded_host.rsplit(":", 1)
- host, port = host.strip(), str(port)
-
- # We trust the port in the Forwarded Host/X-Forwarded-Host over
- # X-Forwarded-Port, or whatever we got from Forwarded
- # Proto/X-Forwarded-Proto.
-
- if forwarded_port != port:
- forwarded_port = port
-
- # We trust the proxy server's forwarded Host
- environ["SERVER_NAME"] = host
- environ["HTTP_HOST"] = forwarded_host
- else:
- # We trust the proxy server's forwarded Host
- environ["SERVER_NAME"] = forwarded_host
- environ["HTTP_HOST"] = forwarded_host
-
- if forwarded_port:
- if forwarded_port not in {"443", "80"}:
- environ["HTTP_HOST"] = "{}:{}".format(
- forwarded_host, forwarded_port
- )
- elif (
- forwarded_port == "80" and environ["wsgi.url_scheme"] != "http"
- ):
- environ["HTTP_HOST"] = "{}:{}".format(
- forwarded_host, forwarded_port
- )
- elif (
- forwarded_port == "443"
- and environ["wsgi.url_scheme"] != "https"
- ):
- environ["HTTP_HOST"] = "{}:{}".format(
- forwarded_host, forwarded_port
- )
-
- if forwarded_port:
- environ["SERVER_PORT"] = str(forwarded_port)
-
- if client_addr:
- def strip_brackets(addr):
- if addr[0] == "[" and addr[-1] == "]":
- return addr[1:-1]
- return addr
-
- if ":" in client_addr and client_addr[-1] != "]":
- addr, port = client_addr.rsplit(":", 1)
- environ["REMOTE_ADDR"] = strip_brackets(addr.strip())
- environ["REMOTE_PORT"] = port.strip()
- else:
- environ["REMOTE_ADDR"] = strip_brackets(client_addr.strip())
-
- return untrusted_headers
-
def get_environment(self):
"""Returns a WSGI environment."""
environ = self.environ
@@ -766,6 +529,14 @@ class WSGITask(Task):
path = path[len(url_prefix):]
environ = {
+ 'REMOTE_ADDR': channel.addr[0],
+ # Nah, we aren't actually going to look up the reverse DNS for
+ # REMOTE_ADDR, but we will happily set this environment variable
+ # for the WSGI application. Spec says we can just set this to
+ # REMOTE_ADDR, so we do.
+ 'REMOTE_HOST': channel.addr[0],
+ # try and set the REMOTE_PORT to something useful, but maybe None
+ 'REMOTE_PORT': str(channel.addr[1]),
'REQUEST_METHOD': request.command.upper(),
'SERVER_PORT': str(server.effective_port),
'SERVER_NAME': server.server_name,
@@ -788,43 +559,12 @@ class WSGITask(Task):
'wsgi.file_wrapper': ReadOnlyFileBasedBuffer,
'wsgi.input_terminated': True, # wsgi.input is EOF terminated
}
- remote_peer = environ['REMOTE_ADDR'] = channel.addr[0]
-
- headers = dict(request.headers)
-
- untrusted_headers = PROXY_HEADERS
- if server.adj.trusted_proxy == '*' or remote_peer == server.adj.trusted_proxy:
- untrusted_headers = self.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=server.adj.trusted_proxy_count,
- trusted_proxy_headers=server.adj.trusted_proxy_headers,
- )
- else:
- # If we are not relying on a proxy, we still want to try and set
- # the REMOTE_PORT to something useful, maybe None though.
- environ["REMOTE_PORT"] = str(channel.addr[1])
-
- # Nah, we aren't actually going to look up the reverse DNS for
- # REMOTE_ADDR, but we will happily set this environment variable for
- # the WSGI application. Spec says we can just set this to REMOTE_ADDR,
- # so we do.
- environ["REMOTE_HOST"] = environ["REMOTE_ADDR"]
-
- # Clear out the untrusted proxy headers
- if server.adj.clear_untrusted_proxy_headers:
- clear_untrusted_headers(
- headers,
- untrusted_headers,
- log_warning=server.adj.log_untrusted_proxy_headers,
- logger=self.logger,
- )
- for key, value in headers.items():
+ for key, value in dict(request.headers).items():
value = value.strip()
mykey = rename_headers.get(key, None)
if mykey is None:
- mykey = 'HTTP_%s' % key
+ mykey = 'HTTP_' + key
if mykey not in environ:
environ[mykey] = value
diff --git a/waitress/tests/fixtureapps/echo.py b/waitress/tests/fixtureapps/echo.py
index f5fd5d1..5509703 100644
--- a/waitress/tests/fixtureapps/echo.py
+++ b/waitress/tests/fixtureapps/echo.py
@@ -1,11 +1,55 @@
-def app(environ, start_response): # pragma: no cover
+from collections import namedtuple
+import json
+
+def app_body_only(environ, start_response): # pragma: no cover
cl = environ.get('CONTENT_LENGTH', None)
if cl is not None:
cl = int(cl)
body = environ['wsgi.input'].read(cl)
cl = str(len(body))
- start_response(
- '200 OK',
- [('Content-Length', cl), ('Content-Type', 'text/plain')]
- )
+ start_response('200 OK', [
+ ('Content-Length', cl),
+ ('Content-Type', 'text/plain'),
+ ])
return [body]
+
+def app(environ, start_response): # pragma: no cover
+ cl = environ.get('CONTENT_LENGTH', None)
+ if cl is not None:
+ cl = int(cl)
+ request_body = environ['wsgi.input'].read(cl)
+ cl = str(len(request_body))
+ meta = {
+ 'method': environ['REQUEST_METHOD'],
+ 'path_info': environ['PATH_INFO'],
+ 'script_name': environ['SCRIPT_NAME'],
+ 'query_string': environ['QUERY_STRING'],
+ 'content_length': cl,
+ 'scheme': environ['wsgi.url_scheme'],
+ 'remote_addr': environ['REMOTE_ADDR'],
+ 'remote_host': environ['REMOTE_HOST'],
+ 'server_port': environ['SERVER_PORT'],
+ 'server_name': environ['SERVER_NAME'],
+ 'headers': {
+ k[len('HTTP_'):]: v
+ for k, v in environ.items()
+ if k.startswith('HTTP_')
+ },
+ }
+ response = json.dumps(meta).encode('utf8') + b'\r\n\r\n' + request_body
+ start_response('200 OK', [
+ ('Content-Length', str(len(response))),
+ ('Content-Type', 'text/plain'),
+ ])
+ return [response]
+
+
+Echo = namedtuple('Echo', (
+ 'method path_info script_name query_string content_length scheme '
+ 'remote_addr remote_host server_port server_name headers body'
+))
+
+def parse_response(response):
+ meta, body = response.split(b'\r\n\r\n', 1)
+ meta = json.loads(meta.decode('utf8'))
+ return Echo(body=body, **meta)
diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py
index f3170b9..0a39771 100644
--- a/waitress/tests/test_functional.py
+++ b/waitress/tests/test_functional.py
@@ -2,6 +2,7 @@ import errno
import logging
import multiprocessing
import os
+import signal
import socket
import string
import subprocess
@@ -28,8 +29,23 @@ def start_server(app, svr, queue, **kwargs): # pragma: no cover
"""Run a fixture application.
"""
logging.getLogger('waitress').addHandler(NullHandler())
+ try_register_coverage()
svr(app, queue, **kwargs).run()
+def try_register_coverage(): # pragma: no cover
+ # Hack around multiprocessing exiting early and not triggering coverage's
+ # atexit handler by trapping the SIGTERM and saving coverage explicitly.
+ if '_COVERAGE_RCFILE' in os.environ:
+ import coverage
+ cov = coverage.Coverage(config_file=os.getenv('_COVERAGE_RCFILE'))
+ cov.start()
+
+ def sigterm(*args):
+ cov.stop()
+ cov.save()
+ sys.exit(0)
+ signal.signal(signal.SIGTERM, sigterm)
+
class FixtureTcpWSGIServer(server.TcpWSGIServer):
"""A version of TcpWSGIServer that relays back what it's bound to.
"""
@@ -139,11 +155,22 @@ class EchoTests(object):
def setUp(self):
from waitress.tests.fixtureapps import echo
- self.start_subprocess(echo.app)
+ self.start_subprocess(
+ echo.app,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'},
+ clear_untrusted_proxy_headers=True,
+ )
def tearDown(self):
self.stop_subprocess()
+ def _read_echo(self, fp):
+ from waitress.tests.fixtureapps import echo
+ line, headers, body = read_http(fp)
+ return line, headers, echo.parse_response(body)
+
def test_date_and_server(self):
to_send = ("GET / HTTP/1.0\n"
"Content-Length: 0\n\n")
@@ -151,7 +178,7 @@ class EchoTests(object):
self.connect()
self.sock.send(to_send)
fp = self.sock.makefile('rb', 0)
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.0')
self.assertEqual(headers.get('server'), 'waitress')
self.assertTrue(headers.get('date'))
@@ -177,10 +204,10 @@ class EchoTests(object):
self.connect()
self.sock.send(to_send)
fp = self.sock.makefile('rb', 0)
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.0')
- self.assertEqual(headers.get('content-length'), '5')
- self.assertEqual(response_body, b'hello')
+ self.assertEqual(echo.content_length, '5')
+ self.assertEqual(echo.body, b'hello')
def test_send_empty_body(self):
to_send = ("GET / HTTP/1.0\n"
@@ -189,10 +216,10 @@ class EchoTests(object):
self.connect()
self.sock.send(to_send)
fp = self.sock.makefile('rb', 0)
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.0')
- self.assertEqual(headers.get('content-length'), '0')
- self.assertEqual(response_body, b'')
+ self.assertEqual(echo.content_length, '0')
+ self.assertEqual(echo.body, b'')
def test_multiple_requests_with_body(self):
orig_sock = self.sock
@@ -222,11 +249,11 @@ class EchoTests(object):
self.connect()
self.sock.send(s)
fp = self.sock.makefile('rb', 0)
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.0')
- self.assertEqual(int(headers['content-length']), len(data))
- self.assertEqual(len(response_body), len(data))
- self.assertEqual(response_body, tobytes(data))
+ self.assertEqual(int(echo.content_length), len(data))
+ self.assertEqual(len(echo.body), len(data))
+ self.assertEqual(echo.body, tobytes(data))
def test_large_body(self):
# 1024 characters.
@@ -240,10 +267,10 @@ class EchoTests(object):
self.connect()
self.sock.send(s)
fp = self.sock.makefile('rb', 0)
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.0')
- self.assertEqual(headers.get('content-length'), '1024')
- self.assertEqual(response_body, tobytes(body))
+ self.assertEqual(echo.content_length, '1024')
+ self.assertEqual(echo.body, tobytes(body))
def test_many_clients(self):
conns = []
@@ -270,10 +297,10 @@ class EchoTests(object):
self.sock.send(header)
self.sock.send(b"0\r\n\r\n")
fp = self.sock.makefile('rb', 0)
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.1')
- self.assertEqual(response_body, b'')
- self.assertEqual(headers['content-length'], '0')
+ self.assertEqual(echo.body, b'')
+ self.assertEqual(echo.content_length, '0')
self.assertFalse('transfer-encoding' in headers)
def test_chunking_request_with_content(self):
@@ -291,10 +318,10 @@ class EchoTests(object):
self.sock.send(control_line)
self.sock.send(s)
self.sock.send(b"0\r\n\r\n")
- line, headers, response_body = read_http(fp)
+ line, headers, echo = self._read_echo(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.1')
- self.assertEqual(response_body, expected)
- self.assertEqual(headers['content-length'], str(len(expected)))
+ self.assertEqual(echo.body, expected)
+ self.assertEqual(echo.content_length, str(len(expected)))
self.assertFalse('transfer-encoding' in headers)
def test_broken_chunked_encoding(self):
@@ -411,11 +438,35 @@ class EchoTests(object):
self.assertEqual(int(response.status), 200)
self.assertEqual(response.getheader('connection'), 'close')
+ def test_proxy_headers(self):
+ to_send = (
+ "GET / HTTP/1.0\n"
+ "Content-Length: 0\n"
+ "Host: www.google.com:8080\n"
+ "X-Forwarded-For: 192.168.1.1\n"
+ "X-Forwarded-Proto: https\n"
+ "X-Forwarded-Port: 5000\n\n"
+ )
+ to_send = tobytes(to_send)
+ self.connect()
+ self.sock.send(to_send)
+ fp = self.sock.makefile('rb', 0)
+ line, headers, echo = self._read_echo(fp)
+ self.assertline(line, '200', 'OK', 'HTTP/1.0')
+ self.assertEqual(headers.get('server'), 'waitress')
+ self.assertTrue(headers.get('date'))
+ self.assertIsNone(echo.headers.get('X_FORWARDED_PORT'))
+ self.assertEqual(echo.headers['HOST'], 'www.google.com:8080')
+ self.assertEqual(echo.scheme, 'https')
+ self.assertEqual(echo.remote_addr, '192.168.1.1')
+ self.assertEqual(echo.remote_host, '192.168.1.1')
+
+
class PipeliningTests(object):
def setUp(self):
from waitress.tests.fixtureapps import echo
- self.start_subprocess(echo.app)
+ self.start_subprocess(echo.app_body_only)
def tearDown(self):
self.stop_subprocess()
@@ -454,7 +505,7 @@ class ExpectContinueTests(object):
def setUp(self):
from waitress.tests.fixtureapps import echo
- self.start_subprocess(echo.app)
+ self.start_subprocess(echo.app_body_only)
def tearDown(self):
self.stop_subprocess()
diff --git a/waitress/tests/test_proxy_headers.py b/waitress/tests/test_proxy_headers.py
new file mode 100644
index 0000000..f3af58a
--- /dev/null
+++ b/waitress/tests/test_proxy_headers.py
@@ -0,0 +1,695 @@
+import unittest
+
+from waitress.compat import tobytes
+
+class TestProxyHeadersMiddleware(unittest.TestCase):
+
+ def _makeOne(self, app, **kw):
+ from waitress.proxy_headers import proxy_headers_middleware
+ return proxy_headers_middleware(app, **kw)
+
+ def _callFUT(self, app, **kw):
+ response = DummyResponse()
+ environ = DummyEnviron(**kw)
+ def start_response(status, response_headers):
+ response.status = status
+ response.headers = response_headers
+ response.steps = list(app(environ, start_response))
+ response.body = b''.join(tobytes(s) for s in response.steps)
+ return response
+
+ def test_get_environment_values_w_scheme_override_untrusted(self):
+ inner = DummyApp()
+ app = self._makeOne(inner)
+ response = self._callFUT(app, headers={
+ 'X_FOO': 'BAR',
+ 'X_FORWARDED_PROTO': 'https',
+ })
+ self.assertEqual(response.status, '200 OK')
+ self.assertEqual(inner.environ['wsgi.url_scheme'], 'http')
+
+ def test_get_environment_values_w_scheme_override_trusted(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='192.168.1.1',
+ trusted_proxy_headers={'x-forwarded-proto'},
+ )
+ response = self._callFUT(
+ app,
+ addr=['192.168.1.1', 8080],
+ headers={
+ 'X_FOO': 'BAR',
+ 'X_FORWARDED_PROTO': 'https',
+ },
+ )
+
+ environ = inner.environ
+ self.assertEqual(response.status, '200 OK')
+ self.assertEqual(environ['SERVER_PORT'], '443')
+ self.assertEqual(environ['SERVER_NAME'], 'localhost')
+ self.assertEqual(environ['REMOTE_ADDR'], '192.168.1.1')
+ self.assertEqual(environ['HTTP_X_FOO'], 'BAR')
+
+ def test_get_environment_values_w_bogus_scheme_override(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='192.168.1.1',
+ trusted_proxy_headers={'x-forwarded-proto'},
+ )
+ response = self._callFUT(
+ app,
+ addr=['192.168.1.1', 80],
+ headers={
+ 'X_FOO': 'BAR',
+ 'X_FORWARDED_PROTO': 'http://p02n3e.com?url=http',
+ },
+ )
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "X-Forwarded-Proto" malformed', response.body)
+
+ def test_get_environment_warning_other_proxy_headers(self):
+ inner = DummyApp()
+ logger = DummyLogger()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='192.168.1.1',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ log_untrusted=True,
+ logger=logger,
+ )
+ response = self._callFUT(
+ app,
+ addr=['192.168.1.1', 80],
+ headers={
+ 'X_FORWARDED_FOR': '[2001:db8::1]',
+ 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
+ },
+ )
+ self.assertEqual(response.status, '200 OK')
+
+ self.assertEqual(len(logger.logged), 1)
+
+ environ = inner.environ
+ self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_get_environment_contains_all_headers_including_untrusted(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='192.168.1.1',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-by'},
+ clear_untrusted=False,
+ )
+ headers_orig = {
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'X_FORWARDED_BY': 'Waitress',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.org',
+ }
+ response = self._callFUT(
+ app,
+ addr=['192.168.1.1', 80],
+ headers=headers_orig.copy(),
+ )
+ self.assertEqual(response.status, '200 OK')
+ environ = inner.environ
+ for k, expected in headers_orig.items():
+ result = environ['HTTP_%s' % k]
+ self.assertEqual(result, expected)
+
+ def test_get_environment_contains_only_trusted_headers(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='192.168.1.1',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-by'},
+ clear_untrusted=True,
+ )
+ response = self._callFUT(
+ app,
+ addr=['192.168.1.1', 80],
+ headers={
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'X_FORWARDED_BY': 'Waitress',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.org',
+ },
+ )
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['HTTP_X_FORWARDED_BY'], 'Waitress')
+ self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_PROTO', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_HOST', environ)
+
+ def test_get_environment_clears_headers_if_untrusted_proxy(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='192.168.1.1',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-by'},
+ clear_untrusted=True,
+ )
+ response = self._callFUT(
+ app,
+ addr=['192.168.1.255', 80],
+ headers={
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'X_FORWARDED_BY': 'Waitress',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.org',
+ },
+ )
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertNotIn('HTTP_X_FORWARDED_BY', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_PROTO', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_HOST', environ)
+
+ def test_parse_proxy_headers_forwarded_for(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_headers={'x-forwarded-for'},
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '192.0.2.1')
+
+ def test_parse_proxy_headers_forwarded_for_v6_missing_brackets(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_headers={'x-forwarded-for'},
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '2001:db8::0'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '2001:db8::0')
+
+ def test_parse_proxy_headers_forwared_for_multiple(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'x-forwarded-for'},
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+
+ def test_parse_forwarded_multiple_proxies_trust_only_two(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': (
+ 'For=192.0.2.1;host=fake.com, '
+ 'For=198.51.100.2;host=example.com:8080, '
+ 'For=203.0.113.1'
+ ),
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_forwarded_multiple_proxies(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': (
+ 'for="[2001:db8::1]:3821";host="example.com:8443";proto="https", '
+ 'for=192.0.2.1;host="example.internal:8080"'
+ ),
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '2001:db8::1')
+ self.assertEqual(environ['REMOTE_PORT'], '3821')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8443')
+ self.assertEqual(environ['SERVER_PORT'], '8443')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_forwarded_multiple_proxies_minimal(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': (
+ 'for="[2001:db8::1]";proto="https", '
+ 'for=192.0.2.1;host="example.org"'
+ ),
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '2001:db8::1')
+ self.assertEqual(environ['SERVER_NAME'], 'example.org')
+ self.assertEqual(environ['HTTP_HOST'], 'example.org')
+ self.assertEqual(environ['SERVER_PORT'], '443')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_proxy_headers_forwarded_host_with_port(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={
+ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com:8080',
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_proxy_headers_forwarded_host_without_port(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={
+ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com',
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com')
+ self.assertEqual(environ['SERVER_PORT'], '80')
+
+ def test_parse_proxy_headers_forwarded_host_with_forwarded_port(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={
+ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host',
+ 'x-forwarded-port',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com',
+ 'X_FORWARDED_PORT': '8080'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_proxy_headers_forwarded_host_multiple_with_forwarded_port(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=2,
+ trusted_proxy_headers={
+ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host',
+ 'x-forwarded-port',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com, example.org',
+ 'X_FORWARDED_PORT': '8080'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_proxy_headers_forwarded_host_multiple_with_forwarded_port_limit_one_trusted(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={
+ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host',
+ 'x-forwarded-port',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com, example.org',
+ 'X_FORWARDED_PORT': '8080'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '203.0.113.1')
+ self.assertEqual(environ['SERVER_NAME'], 'example.org')
+ self.assertEqual(environ['HTTP_HOST'], 'example.org:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_forwarded(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': 'For=198.51.100.2:5858;host=example.com:8080;proto=https',
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['REMOTE_PORT'], '5858')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_forwarded_empty_pair(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': 'For=198.51.100.2;;proto=https;by=_unused',
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+
+ def test_parse_forwarded_pair_token_whitespace(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': 'For=198.51.100.2; proto =https',
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "Forwarded" malformed', response.body)
+
+ def test_parse_forwarded_pair_value_whitespace(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': 'For= "198.51.100.2"; proto =https',
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "Forwarded" malformed', response.body)
+
+ def test_parse_forwarded_pair_no_equals(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': 'For'
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "Forwarded" malformed', response.body)
+
+ def test_parse_forwarded_warning_unknown_token(self):
+ inner = DummyApp()
+ logger = DummyLogger()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'},
+ logger=logger,
+ )
+ response = self._callFUT(app, headers={
+ 'FORWARDED': (
+ 'For=198.51.100.2;host=example.com:8080;proto=https;'
+ 'unknown="yolo"'
+ ),
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ self.assertEqual(len(logger.logged), 1)
+ self.assertIn('Unknown Forwarded token', logger.logged[0])
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_no_valid_proxy_headers(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['REMOTE_ADDR'], '127.0.0.1')
+ self.assertEqual(environ['SERVER_NAME'], 'localhost')
+ self.assertEqual(environ['HTTP_HOST'], '192.168.1.1:80')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'http')
+
+ def test_parse_multiple_x_forwarded_proto(self):
+ inner = DummyApp()
+ logger = DummyLogger()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-proto'},
+ logger=logger,
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_PROTO': 'http, https',
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "X-Forwarded-Proto" malformed', response.body)
+
+ def test_parse_multiple_x_forwarded_port(self):
+ inner = DummyApp()
+ logger = DummyLogger()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-port'},
+ logger=logger,
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_PORT': '443, 80',
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "X-Forwarded-Port" malformed', response.body)
+
+ def test_parse_forwarded_port_wrong_proto_port_80(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={
+ 'x-forwarded-port', 'x-forwarded-host', 'x-forwarded-proto',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_PORT': '80',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.com',
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:80')
+ self.assertEqual(environ['SERVER_PORT'], '80')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_forwarded_port_wrong_proto_port_443(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={
+ 'x-forwarded-port', 'x-forwarded-host', 'x-forwarded-proto',
+ },
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_PORT': '443',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com',
+ })
+ self.assertEqual(response.status, '200 OK')
+
+ environ = inner.environ
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:443')
+ self.assertEqual(environ['SERVER_PORT'], '443')
+ self.assertEqual(environ['wsgi.url_scheme'], 'http')
+
+ def test_parse_forwarded_for_bad_quote(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-for'},
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_FOR': '"foo'
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "X-Forwarded-For" malformed', response.body)
+
+ def test_parse_forwarded_host_bad_quote(self):
+ inner = DummyApp()
+ app = self._makeOne(
+ inner,
+ trusted_proxy='*',
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-host'},
+ )
+ response = self._callFUT(app, headers={
+ 'X_FORWARDED_HOST': '"foo'
+ })
+ self.assertEqual(response.status, '400 Bad Request')
+ self.assertIn(b'Header "X-Forwarded-Host" malformed', response.body)
+
+
+class DummyLogger(object):
+ def __init__(self):
+ self.logged = []
+
+ def warning(self, msg, *args):
+ self.logged.append(msg % args)
+
+
+class DummyApp(object):
+ def __call__(self, environ, start_response):
+ self.environ = environ
+ start_response('200 OK', [('Content-Type', 'text/plain')])
+ yield 'hello'
+
+
+class DummyResponse(object):
+ status = None
+ headers = None
+ body = None
+
+
+def DummyEnviron(
+ addr=('127.0.0.1', 8080),
+ scheme='http',
+ server='localhost',
+ headers=None,
+):
+ environ = {
+ 'REMOTE_ADDR': addr[0],
+ 'REMOTE_HOST': addr[0],
+ 'REMOTE_PORT': addr[1],
+ 'SERVER_PORT': str(addr[1]),
+ 'SERVER_NAME': server,
+ 'wsgi.url_scheme': scheme,
+ 'HTTP_HOST': '192.168.1.1:80',
+ }
+ if headers:
+ environ.update({
+ 'HTTP_' + key.upper().replace('-', '_'): value
+ for key, value in headers.items()
+ })
+ return environ
diff --git a/waitress/tests/test_task.py b/waitress/tests/test_task.py
index ffc34b7..a75b87f 100644
--- a/waitress/tests/test_task.py
+++ b/waitress/tests/test_task.py
@@ -770,603 +770,6 @@ class TestWSGITask(unittest.TestCase):
self.assertEqual(environ['wsgi.input_terminated'], True)
self.assertEqual(inst.environ, environ)
- def test_get_environment_values_w_scheme_override_untrusted(self):
- inst = self._makeOne()
- request = DummyParser()
- request.headers = {
- 'CONTENT_TYPE': 'abc',
- 'CONTENT_LENGTH': '10',
- 'X_FOO': 'BAR',
- 'X_FORWARDED_PROTO': 'https',
- 'CONNECTION': 'close',
- }
- request.query = 'abc'
- inst.request = request
- environ = inst.get_environment()
- self.assertEqual(environ['wsgi.url_scheme'], 'http')
-
- def test_get_environment_values_w_scheme_override_trusted(self):
- import sys
- inst = self._makeOne()
- inst.channel.addr = ['192.168.1.1', 8080]
- inst.channel.server.adj.trusted_proxy = '192.168.1.1'
- inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-proto'}
- request = DummyParser()
- request.headers = {
- 'CONTENT_TYPE': 'abc',
- 'CONTENT_LENGTH': '10',
- 'X_FOO': 'BAR',
- 'X_FORWARDED_PROTO': 'https',
- 'CONNECTION': 'close',
- }
- request.query = 'abc'
- inst.request = request
- environ = inst.get_environment()
-
- # nail the keys of environ
- self.assertEqual(sorted(environ.keys()), [
- 'CONTENT_LENGTH', 'CONTENT_TYPE', 'HTTP_CONNECTION', 'HTTP_X_FOO',
- 'HTTP_X_FORWARDED_PROTO', 'PATH_INFO', 'QUERY_STRING',
- 'REMOTE_ADDR', 'REMOTE_HOST', 'REQUEST_METHOD', 'SCRIPT_NAME',
- 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SOFTWARE',
- 'wsgi.errors', 'wsgi.file_wrapper', 'wsgi.input',
- 'wsgi.input_terminated', 'wsgi.multiprocess', 'wsgi.multithread',
- 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version'
- ])
-
- self.assertEqual(environ['REQUEST_METHOD'], 'GET')
- self.assertEqual(environ['SERVER_PORT'], '443')
- self.assertEqual(environ['SERVER_NAME'], 'localhost')
- self.assertEqual(environ['SERVER_SOFTWARE'], 'waitress')
- self.assertEqual(environ['SERVER_PROTOCOL'], 'HTTP/1.0')
- self.assertEqual(environ['SCRIPT_NAME'], '')
- self.assertEqual(environ['HTTP_CONNECTION'], 'close')
- self.assertEqual(environ['PATH_INFO'], '/')
- self.assertEqual(environ['QUERY_STRING'], 'abc')
- self.assertEqual(environ['REMOTE_ADDR'], '192.168.1.1')
- self.assertEqual(environ['CONTENT_TYPE'], 'abc')
- self.assertEqual(environ['CONTENT_LENGTH'], '10')
- self.assertEqual(environ['HTTP_X_FOO'], 'BAR')
- self.assertEqual(environ['wsgi.version'], (1, 0))
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
- self.assertEqual(environ['wsgi.errors'], sys.stderr)
- self.assertEqual(environ['wsgi.multithread'], True)
- self.assertEqual(environ['wsgi.multiprocess'], False)
- self.assertEqual(environ['wsgi.run_once'], False)
- self.assertEqual(environ['wsgi.input'], 'stream')
- self.assertEqual(environ['wsgi.input_terminated'], True)
- self.assertEqual(inst.environ, environ)
-
- def test_get_environment_values_w_bogus_scheme_override(self):
- inst = self._makeOne()
- inst.channel.addr = ['192.168.1.1', 80]
- inst.channel.server.adj.trusted_proxy = '192.168.1.1'
- inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-proto'}
- request = DummyParser()
- request.headers = {
- 'CONTENT_TYPE': 'abc',
- 'CONTENT_LENGTH': '10',
- 'X_FOO': 'BAR',
- 'X_FORWARDED_PROTO': 'http://p02n3e.com?url=http',
- 'CONNECTION': 'close',
- }
- request.query = 'abc'
- inst.request = request
- self.assertRaises(ValueError, inst.get_environment)
-
- def test_get_environment_warning_other_proxy_headers(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- inst.request.headers = {
- 'X_FORWARDED_FOR': '[2001:db8::1]',
- 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
- }
- inst.channel.addr = ['192.168.1.1', 80]
- inst.channel.server.adj.trusted_proxy = '192.168.1.1'
- inst.channel.server.adj.trusted_proxy_count = 1
- inst.channel.server.adj.trusted_proxy_headers = {'forwarded'}
- inst.channel.server.adj.log_untrusted_proxy_headers = True
- environ = inst.get_environment()
-
- self.assertEqual(len(inst.logger.logged), 1)
- self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
-
- def test_get_environment_contains_all_headers_including_untrusted(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- inst.request.headers = {
- 'X_FORWARDED_FOR': '198.51.100.2',
- 'X_FORWARDED_BY': 'Waitress',
- 'X_FORWARDED_PROTO': 'https',
- 'X_FORWARDED_HOST': 'example.org',
- }
- headers_orig = inst.request.headers.copy()
- inst.channel.addr = ['192.168.1.1', 80]
- inst.channel.server.adj.trusted_proxy = '192.168.1.1'
- inst.channel.server.adj.trusted_proxy_count = 1
- inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-by'}
- inst.channel.server.adj.clear_untrusted_proxy_headers = False
- environ = inst.get_environment()
-
- for k, expected in headers_orig.items():
- result = environ['HTTP_%s' % k]
- self.assertEqual(result, expected)
-
- def test_get_environment_contains_only_trusted_headers(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- inst.request.headers = {
- 'X_FORWARDED_FOR': '198.51.100.2',
- 'X_FORWARDED_BY': 'Waitress',
- 'X_FORWARDED_PROTO': 'https',
- 'X_FORWARDED_HOST': 'example.org',
- }
- inst.channel.addr = ['192.168.1.1', 80]
- inst.channel.server.adj.trusted_proxy = '192.168.1.1'
- inst.channel.server.adj.trusted_proxy_count = 1
- inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-by'}
- inst.channel.server.adj.clear_untrusted_proxy_headers = True
- environ = inst.get_environment()
-
- self.assertEqual(environ['HTTP_X_FORWARDED_BY'], 'Waitress')
- self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
- self.assertNotIn('HTTP_X_FORWARDED_PROTO', environ)
- self.assertNotIn('HTTP_X_FORWARDED_HOST', environ)
-
- def test_get_environment_clears_headers_if_untrusted_proxy(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- inst.request.headers = {
- 'X_FORWARDED_FOR': '198.51.100.2',
- 'X_FORWARDED_BY': 'Waitress',
- 'X_FORWARDED_PROTO': 'https',
- 'X_FORWARDED_HOST': 'example.org',
- }
- inst.channel.addr = ['192.168.1.255', 80]
- inst.channel.server.adj.trusted_proxy = '192.168.1.1'
- inst.channel.server.adj.trusted_proxy_count = 1
- inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-by'}
- inst.channel.server.adj.clear_untrusted_proxy_headers = True
- environ = inst.get_environment()
-
- self.assertNotIn('HTTP_X_FORWARDED_BY', environ)
- self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
- self.assertNotIn('HTTP_X_FORWARDED_PROTO', environ)
- self.assertNotIn('HTTP_X_FORWARDED_HOST', environ)
-
- def test_parse_proxy_headers_forwarded_for(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-for'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '192.0.2.1')
-
- def test_parse_proxy_headers_forwarded_for_v6_missing_brackets(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '2001:db8::0'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-for'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '2001:db8::0')
-
- def test_parse_proxy_headers_forwared_for_multiple(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'x-forwarded-for'}
- )
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
-
- def test_parse_forwarded_multiple_proxies_trust_only_two(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'For=192.0.2.1;host=fake.com, For=198.51.100.2;host=example.com:8080, For=203.0.113.1'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'forwarded'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
-
- def test_parse_forwarded_multiple_proxies(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'for="[2001:db8::1]:3821";host="example.com:8443";proto="https", for=192.0.2.1;host="example.internal:8080"'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'forwarded'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '2001:db8::1')
- self.assertEqual(environ['REMOTE_PORT'], '3821')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8443')
- self.assertEqual(environ['SERVER_PORT'], '8443')
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
-
- def test_parse_forwarded_multiple_proxies_minimal(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'for="[2001:db8::1]";proto="https", for=192.0.2.1;host="example.org"'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'forwarded'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '2001:db8::1')
- self.assertEqual(environ['SERVER_NAME'], 'example.org')
- self.assertEqual(environ['HTTP_HOST'], 'example.org')
- self.assertEqual(environ['SERVER_PORT'], '443')
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
-
- def test_parse_proxy_headers_forwarded_host_with_port(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
- 'X_FORWARDED_PROTO': 'http',
- 'X_FORWARDED_HOST': 'example.com:8080',
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
-
- def test_parse_proxy_headers_forwarded_host_without_port(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
- 'X_FORWARDED_PROTO': 'http',
- 'X_FORWARDED_HOST': 'example.com',
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com')
- self.assertEqual(environ['SERVER_PORT'], '80')
-
- def test_parse_proxy_headers_forwarded_host_with_forwarded_port(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
- 'X_FORWARDED_PROTO': 'http',
- 'X_FORWARDED_HOST': 'example.com',
- 'X_FORWARDED_PORT': '8080'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
-
- def test_parse_proxy_headers_forwarded_host_multiple_with_forwarded_port(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
- 'X_FORWARDED_PROTO': 'http',
- 'X_FORWARDED_HOST': 'example.com, example.org',
- 'X_FORWARDED_PORT': '8080'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=2,
- trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
-
- def test_parse_proxy_headers_forwarded_host_multiple_with_forwarded_port_limit_one_trusted(self):
- inst = self._makeOne()
-
- headers = {
- 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
- 'X_FORWARDED_PROTO': 'http',
- 'X_FORWARDED_HOST': 'example.com, example.org',
- 'X_FORWARDED_PORT': '8080'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '203.0.113.1')
- self.assertEqual(environ['SERVER_NAME'], 'example.org')
- self.assertEqual(environ['HTTP_HOST'], 'example.org:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
-
- def test_parse_forwarded(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'For=198.51.100.2:5858;host=example.com:8080;proto=https'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'forwarded'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['REMOTE_PORT'], '5858')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
-
- def test_parse_forwarded_empty_pair(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'For=198.51.100.2;;proto=https;by=_unused'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'forwarded'}
- )
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
-
- def test_parse_forwarded_pair_token_whitespace(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'For=198.51.100.2; proto =https'
- }
- environ = {}
-
- with self.assertRaises(ValueError):
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'forwarded'}
- )
-
- def test_parse_forwarded_pair_value_whitespace(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'For= "198.51.100.2"; proto =https'
- }
- environ = {}
-
- with self.assertRaises(ValueError):
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'forwarded'}
- )
-
- def test_parse_forwarded_pair_no_equals(self):
- inst = self._makeOne()
-
- headers = {
- 'FORWARDED': 'For'
- }
- environ = {}
-
- with self.assertRaises(ValueError):
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'forwarded'}
- )
-
- def test_parse_forwarded_warning_unknown_token(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- headers = {
- 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https;unknown="yolo"'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'forwarded'}
- )
-
- self.assertEqual(len(inst.logger.logged), 1)
- self.assertIn('Unknown Forwarded token', inst.logger.logged[0])
-
- self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
- self.assertEqual(environ['SERVER_PORT'], '8080')
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
-
- def test_parse_no_valid_proxy_headers(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- headers = {
- 'X_FORWARDED_FOR': '198.51.100.2',
- 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- )
-
- self.assertEqual(environ, {})
-
- def test_parse_multiple_x_forwarded_proto(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- headers = {
- 'X_FORWARDED_PROTO': 'http, https',
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-proto'}
- )
-
- self.assertEqual(environ, {})
- self.assertEqual(len(inst.logger.logged), 1)
- self.assertIn("Found multiple values in X-Forwarded-Proto", inst.logger.logged[0])
-
- def test_parse_multiple_x_forwarded_port(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- headers = {
- 'X_FORWARDED_PORT': '443, 80',
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-port'}
- )
-
- self.assertEqual(environ, {})
- self.assertEqual(len(inst.logger.logged), 1)
- self.assertIn("Found multiple values in X-Forwarded-Port", inst.logger.logged[0])
-
- def test_parse_forwarded_port_wrong_proto_port_80(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- headers = {
- 'X_FORWARDED_PORT': '80',
- 'X_FORWARDED_PROTO': 'https',
- 'X_FORWARDED_HOST': 'example.com',
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-port', 'x-forwarded-host', 'x-forwarded-proto'}
- )
-
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:80')
- self.assertEqual(environ['SERVER_PORT'], '80')
- self.assertEqual(environ['wsgi.url_scheme'], 'https')
-
- def test_parse_forwarded_port_wrong_proto_port_443(self):
- inst = self._makeOne()
- inst.logger = DummyLogger()
-
- headers = {
- 'X_FORWARDED_PORT': '443',
- 'X_FORWARDED_PROTO': 'http',
- 'X_FORWARDED_HOST': 'example.com',
- }
- environ = {}
- inst.parse_proxy_headers(
- environ,
- headers,
- trusted_proxy_count=1,
- trusted_proxy_headers={'x-forwarded-port', 'x-forwarded-host', 'x-forwarded-proto'}
- )
-
- self.assertEqual(environ['SERVER_NAME'], 'example.com')
- self.assertEqual(environ['HTTP_HOST'], 'example.com:443')
- self.assertEqual(environ['SERVER_PORT'], '443')
- self.assertEqual(environ['wsgi.url_scheme'], 'http')
-
class TestErrorTask(unittest.TestCase):
@@ -1375,10 +778,17 @@ class TestErrorTask(unittest.TestCase):
channel = DummyChannel()
if request is None:
request = DummyParser()
- request.error = DummyError()
+ request.error = self._makeDummyError()
from waitress.task import ErrorTask
return ErrorTask(channel, request)
+ def _makeDummyError(self):
+ from waitress.utilities import Error
+ e = Error('body')
+ e.code = 432
+ e.reason = 'Too Ugly'
+ return e
+
def test_execute_http_10(self):
inst = self._makeOne()
inst.execute()
@@ -1442,12 +852,6 @@ class TestErrorTask(unittest.TestCase):
self.assertEqual(lines[6], b'body')
self.assertEqual(lines[7], b'(generated by waitress)')
-
-class DummyError(object):
- code = '432'
- reason = 'Too Ugly'
- body = 'body'
-
class DummyTask(object):
serviced = False
cancelled = False
@@ -1464,11 +868,6 @@ class DummyAdj(object):
host = '127.0.0.1'
port = 80
url_prefix = ''
- trusted_proxy = None
- trusted_proxy_count = 1
- trusted_proxy_headers = set()
- log_untrusted_proxy_headers = True
- clear_untrusted_proxy_headers = True
class DummyServer(object):
server_name = 'localhost'
diff --git a/waitress/utilities.py b/waitress/utilities.py
index a21ca4e..f06bdc9 100644
--- a/waitress/utilities.py
+++ b/waitress/utilities.py
@@ -21,20 +21,10 @@ import os
import re
import stat
import time
-from collections import namedtuple
logger = logging.getLogger('waitress')
queue_logger = logging.getLogger('waitress.queue')
-PROXY_HEADERS = frozenset({
- 'X_FORWARDED_FOR',
- 'X_FORWARDED_HOST',
- 'X_FORWARDED_PROTO',
- 'X_FORWARDED_PORT',
- 'X_FORWARDED_BY',
- 'FORWARDED',
-})
-
def find_double_newline(s):
"""Returns the position just after a double newline in the given string."""
pos1 = s.find(b'\n\r\n') # One kind of double newline
@@ -119,17 +109,19 @@ def unpack_rfc822(m):
# rfc850 format
rfc850_date = join(
- [concat(long_day_reg, ','),
- join(
- [group('[0-9][0-9]?'),
- months_reg,
- group('[0-9]+')
- ],
- '-'
- ),
- hms_reg,
- 'gmt'
- ],
+ [
+ concat(long_day_reg, ','),
+ join(
+ [
+ group('[0-9][0-9]?'),
+ months_reg,
+ group('[0-9]+')
+ ],
+ '-'
+ ),
+ hms_reg,
+ 'gmt'
+ ],
' '
)
@@ -180,42 +172,6 @@ def parse_http_date(d):
return retval
-def cleanup_unix_socket(path):
- try:
- st = os.stat(path)
- except OSError as exc:
- if exc.errno != errno.ENOENT:
- raise # pragma: no cover
- else:
- if stat.S_ISSOCK(st.st_mode):
- try:
- os.remove(path)
- except OSError: # pragma: no cover
- # avoid race condition error during tests
- pass
-
-class Error(object):
-
- def __init__(self, body):
- self.body = body
-
-class BadRequest(Error):
- code = 400
- reason = 'Bad Request'
-
-class RequestHeaderFieldsTooLarge(BadRequest):
- code = 431
- reason = 'Request Header Fields Too Large'
-
-class RequestEntityTooLarge(BadRequest):
- code = 413
- reason = 'Request Entity Too Large'
-
-class InternalServerError(Error):
- code = 500
- reason = 'Internal Server Error'
-
-
# RFC 5234 Appendix B.1 "Core Rules":
# VCHAR = %x21-7E
# ; visible (printing) characters
@@ -259,25 +215,50 @@ def undquote(value):
raise ValueError('Invalid quoting in value')
-Forwarded = namedtuple('Forwarded', ['by', 'for_', 'host', 'proto'])
-
-
-def clear_untrusted_headers(
- headers, untrusted_headers, log_warning=False, logger=logger
-):
- untrusted_headers_removed = [
- header
- for header in untrusted_headers
- if headers.pop(header, False) is not False
- ]
-
- if log_warning and untrusted_headers_removed:
- untrusted_headers_removed = [
- "-".join([x.capitalize() for x in header.split("_")])
- for header in untrusted_headers_removed
- ]
- logger.warning(
- "Removed untrusted headers (%s). Waitress recommends these be "
- "removed upstream.",
- ", ".join(untrusted_headers_removed),
- )
+def cleanup_unix_socket(path):
+ try:
+ st = os.stat(path)
+ except OSError as exc:
+ if exc.errno != errno.ENOENT:
+ raise # pragma: no cover
+ else:
+ if stat.S_ISSOCK(st.st_mode):
+ try:
+ os.remove(path)
+ except OSError: # pragma: no cover
+ # avoid race condition error during tests
+ pass
+
+class Error(object):
+
+ def __init__(self, body):
+ self.body = body
+
+ def to_response(self):
+ status = '%s %s' % (self.code, self.reason)
+ body = '%s\r\n\r\n%s' % (self.reason, self.body)
+ tag = '\r\n\r\n(generated by waitress)'
+ body = body + tag
+ headers = [('Content-Type', 'text/plain')]
+ return status, headers, body
+
+ def wsgi_response(self, environ, start_response):
+ status, headers, body = self.to_response()
+ start_response(status, headers)
+ yield body
+
+class BadRequest(Error):
+ code = 400
+ reason = 'Bad Request'
+
+class RequestHeaderFieldsTooLarge(BadRequest):
+ code = 431
+ reason = 'Request Header Fields Too Large'
+
+class RequestEntityTooLarge(BadRequest):
+ code = 413
+ reason = 'Request Entity Too Large'
+
+class InternalServerError(Error):
+ code = 500
+ reason = 'Internal Server Error'