summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/contrib/test_fixers.py35
-rw-r--r--tests/test_wsgi.py66
-rw-r--r--werkzeug/contrib/fixers.py223
-rw-r--r--werkzeug/wsgi.py25
4 files changed, 256 insertions, 93 deletions
diff --git a/tests/contrib/test_fixers.py b/tests/contrib/test_fixers.py
index 0ff91a93..a6a29956 100644
--- a/tests/contrib/test_fixers.py
+++ b/tests/contrib/test_fixers.py
@@ -158,6 +158,41 @@ class TestServerFixer(object):
assert wsgi_headers['Location'] == '{}/foo/bar.hml'.format(
assumed_host)
+ @pytest.mark.parametrize(('environ', 'remote_addr', 'url_root'), (
+ pytest.param({
+ 'REMOTE_ADDR': '192.168.0.1',
+ 'HTTP_HOST': 'spam',
+ }, '192.168.0.1', 'http://spam/', id='basic'),
+ ))
+ def test_proxy_fix_new(self, environ, remote_addr, url_root):
+ map = Map([Rule('/parrot', endpoint='parrot')])
+
+ @Request.application
+ def app(request):
+ assert request.remote_addr == remote_addr
+ assert request.url_root == url_root
+
+ urls = map.bind_to_environ(request.environ)
+ assert urls.build('parrot') == '/'.join((
+ request.script_root, 'parrot'))
+ assert urls.match('/parrot')[0] == 'parrot'
+
+ return Response('success')
+
+ app = fixers.ProxyFix(app)
+
+ environ = create_environ(environ_overrides=environ)
+ if 'HTTP_HOST' not in environ:
+ del environ['HTTP_HOST']
+
+ response = Response.from_app(app, environ)
+ assert response.get_data() == b'success'
+
+ redirect_app = redirect('/parrot')
+ response = Response.from_app(redirect_app, environ)
+ location = response.get_wsgi_headers(environ)['Location']
+ assert location == '/'.join(url_root, )
+
def test_proxy_fix_forwarded_prefix(self):
@fixers.ProxyFix
@Request.application
diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py
index 4fd99ed1..1e3c5ba8 100644
--- a/tests/test_wsgi.py
+++ b/tests/test_wsgi.py
@@ -98,32 +98,46 @@ def test_dispatchermiddleware():
assert b''.join(app_iter).strip() == b'NOT FOUND'
-def test_get_host_by_http_host():
- env = {'HTTP_HOST': 'example.org', 'wsgi.url_scheme': 'http'}
- assert wsgi.get_host(env) == 'example.org'
- env['HTTP_HOST'] = 'example.org:8080'
- assert wsgi.get_host(env) == 'example.org:8080'
- env['HOST_NAME'] = 'ignore me'
- assert wsgi.get_host(env) == 'example.org:8080'
-
-
-def test_get_host_by_server_name_and_port():
- env = {'SERVER_NAME': 'example.org', 'SERVER_PORT': '80',
- 'wsgi.url_scheme': 'http'}
- assert wsgi.get_host(env) == 'example.org'
- env['wsgi.url_scheme'] = 'https'
- assert wsgi.get_host(env) == 'example.org:80'
- env['SERVER_PORT'] = '8080'
- assert wsgi.get_host(env) == 'example.org:8080'
- env['SERVER_PORT'] = '443'
- assert wsgi.get_host(env) == 'example.org'
-
-
-def test_get_host_ignore_x_forwarded_for():
- env = {'HTTP_X_FORWARDED_HOST': 'forwarded',
- 'HTTP_HOST': 'example.org',
- 'wsgi.url_scheme': 'http'}
- assert wsgi.get_host(env) == 'example.org'
+@pytest.mark.parametrize(('environ', 'expect'), (
+ pytest.param({
+ 'HTTP_HOST': 'spam',
+ }, 'spam', id='host'),
+ pytest.param({
+ 'HTTP_HOST': 'spam:80',
+ }, 'spam', id='host, strip http port'),
+ pytest.param({
+ 'wsgi.url_scheme': 'https',
+ 'HTTP_HOST': 'spam:443',
+ }, 'spam', id='host, strip https port'),
+ pytest.param({
+ 'HTTP_HOST': 'spam:8080',
+ }, 'spam:8080', id='host, custom port'),
+ pytest.param({
+ 'HTTP_HOST': 'spam',
+ 'SERVER_NAME': 'eggs',
+ 'SERVER_PORT': '80',
+ }, 'spam', id='prefer host'),
+ pytest.param({
+ 'SERVER_NAME': 'eggs',
+ 'SERVER_PORT': '80'
+ }, 'eggs', id='name, ignore http port'),
+ pytest.param({
+ 'wsgi.url_scheme': 'https',
+ 'SERVER_NAME': 'eggs',
+ 'SERVER_PORT': '443'
+ }, 'eggs', id='name, ignore https port'),
+ pytest.param({
+ 'SERVER_NAME': 'eggs',
+ 'SERVER_PORT': '8080'
+ }, 'eggs:8080', id='name, custom port'),
+ pytest.param({
+ 'HTTP_HOST': 'ham',
+ 'HTTP_X_FORWARDED_HOST': 'eggs'
+ }, 'ham', id='ignore x-forwarded-host'),
+))
+def test_get_host(environ, expect):
+ environ.setdefault('wsgi.url_scheme', 'http')
+ assert wsgi.get_host(environ) == expect
def test_get_host_validate_trusted_hosts():
diff --git a/werkzeug/contrib/fixers.py b/werkzeug/contrib/fixers.py
index 86e977ba..735df329 100644
--- a/werkzeug/contrib/fixers.py
+++ b/werkzeug/contrib/fixers.py
@@ -16,6 +16,8 @@
:copyright: Copyright 2009 by the Werkzeug Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
+import warnings
+
try:
from urllib import unquote
except ImportError:
@@ -95,76 +97,185 @@ class PathInfoFromRequestUriFix(object):
class ProxyFix(object):
+ """Adjust the WSGI environ based on ``Forwarded`` headers that
+ proxies in front of the application may set.
+
+ When the application is running behind a server like Nginx (or
+ another server or proxy), WSGI will see the request as coming from
+ that server rather than the real client. Proxies set various headers
+ to track where the request actually came from.
+
+ This middleware should only be applied if the application is
+ actually behind such a proxy, and should be configured with the
+ number of proxies that are chained in front of it. Not all proxies
+ set all the headers. Since incoming headers can be faked, you must
+ set how many proxies are setting each header so the middleware knows
+ what to trust.
+
+ The original values of the headers are stored in the WSGI
+ environ as ``werkzeug.proxy_fix.orig``, a dict.
+
+ :param app: The WSGI application.
+ :param x_for: Number of values to trust for ``X-Forwarded-For``.
+ :param x_proto: Number of values to trust for ``X-Forwarded-Proto``.
+ :param x_host: Number of values to trust for ``X-Forwarded-Host``.
+ :param x_port: Number of values to trust for ``X-Forwarded-Port``.
+ :param x_prefix: Number of values to trust for
+ ``X-Forwarded-Prefix``.
+ :param num_proxies: Deprecated, use ``x_for`` instead.
+
+ .. versionchanged:: 0.15
+ Support ``X-Forwarded-Proto``, ``X-Forwarded-Port``,
+ ``X-Forwarded-Prefix``.
+
+ .. versionchanged:: 0.15
+ All headers support multiple values. Previously only
+ ``X-Forwarded-For`` parsed multiple values.
+
+ .. versionchanged:: 0.15
+ Deprecate ``num_proxies`` argument. Each header is configured
+ with a separate number of trusted proxies.
+
+ .. versionchanged:: 0.15
+ Original WSGI environ values are stored in the
+ ``werkzeug.proxy_fix.orig`` dict. Previously they were stored
+ under separate keys.
+ """
- """This middleware can be applied to add HTTP proxy support to an
- application that was not designed with HTTP proxies in mind. It
- sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers. While
- Werkzeug-based applications already can use
- :py:func:`werkzeug.wsgi.get_host` to retrieve the current host even if
- behind proxy setups, this middleware can be used for applications which
- access the WSGI environment directly.
+ def __init__(
+ self, app, num_proxies=None,
+ x_for=1, x_proto=0, x_host=0, x_port=0, x_prefix=0
+ ):
+ self.app = app
+ self.trusted = {
+ 'x_for': x_for,
+ 'x_proto': x_proto,
+ 'x_host': x_host,
+ 'x_port': x_port,
+ 'x_prefix': x_prefix,
+ }
+ self.num_proxies = num_proxies
- If you have more than one proxy server in front of your app, set
- `num_proxies` accordingly.
+ @property
+ def num_proxies(self):
+ """The number of proxies setting ``X-Forwarded-For`` in front
+ of the application.
- Do not use this middleware in non-proxy setups for security reasons.
+ .. deprecated:: 0.15
+ A separate number of trusted proxies for each header is
+ stored in :attr:`trusted`. ``num_proxies`` maps to the
+ ``x_for`` key.
- The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in
- the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and
- `werkzeug.proxy_fix.orig_http_host`.
+ :internal:
+ """
+ warnings.warn(DeprecationWarning(
+ "num_proxies is deprecated. Use trusted['x_for'] instead."))
+ return self.trusted['x_for']
+
+ @num_proxies.setter
+ def num_proxies(self, value):
+ if value is not None:
+ warnings.warn(DeprecationWarning(
+ 'num_proxies is deprecated. Use x_for and other header'
+ ' offsets instead.'))
+ self.trusted['x_for'] = value
- :param app: the WSGI application
- :param num_proxies: the number of proxy servers in front of the app.
- """
+ def get_remote_addr(self, forwarded_for):
+ """Get the real ``remote_addr`` by looking backwards ``x_for``
+ number of values in the ``X-Forwarded-For`` header.
- def __init__(self, app, num_proxies=1):
- self.app = app
- self.num_proxies = num_proxies
+ :param forwarded_for: List of values parsed from the
+ ``X-Forwarded-For`` header.
+ :return: The real ``remote_addr``, or ``None`` if there were not
+ at least ``x_for`` values.
- def get_remote_addr(self, forwarded_for):
- """Selects the new remote addr from the given list of ips in
- X-Forwarded-For. By default it picks the one that the `num_proxies`
- proxy server provides. Before 0.9 it would always pick the first.
+ .. deprecated:: 0.15
+ Use :meth:`get_by_offset('x_for', values) <get_by_offset>`
+ instead.
+
+ .. versionchanged:: 0.9
+ Use ``num_proxies`` instead of always picking the first
+ value.
.. versionadded:: 0.8
"""
- if len(forwarded_for) >= self.num_proxies:
- return forwarded_for[-self.num_proxies]
+ warnings.warn(DeprecationWarning(
+ "get_remote_addr is deprecated. Use"
+ " get_by_offset('x_for', values) instead."))
+ return self.get_by_offset('x_for', forwarded_for)
+
+ def get_by_offset(self, key, values):
+ """Get the real value from a list of values parsed from a header
+ based on the configured number of trusted proxies.
+
+ :param key: Key to get from :attr:`trusted`.
+ :param values: List of values parsed from a header.
+ :return: The real value, or ``None`` if there are fewer values
+ than the number of trusted proxies.
+
+ .. versionadded:: 0.15
+ """
+ offset = self.trusted[key]
+ if len(values) >= offset:
+ return values[-offset]
+
+ def set_x_host(self, value, environ):
+ """Set ``HTTP_HOST`` and ``SERVER_NAME`` based on
+ ``X-Forwarded-Host``.
+
+ :internal:
+ """
+ environ['HTTP_HOST'] = value
+ environ['SERVER_NAME'] = value
+
+ def set_x_port(self, value, environ):
+ """Set ``HTTP_HOST`` and ``SERVER_PORT`` based on
+ ``X-Forwarded-Port``.
+
+ If ``HTTP_HOST`` is not set, it will be skipped. Otherwise the
+ port will be added or changed.
+
+ :internal:
+ """
+ host = environ.get('HTTP_HOST')
+ if host:
+ parts = host.split(':', 1)
+ host = parts[0] if len(parts) == 2 else host
+ environ['HTTP_HOST'] = '%s:%s' % (host, value)
+
+ environ['SERVER_PORT'] = value
def __call__(self, environ, start_response):
+ """Modify the WSGI environ based on the various ``Forwarded``
+ headers before calling the wrapped application. Store the
+ original environ values in ``werkzeug.proxy_fix.orig_{key}``.
+ """
getter = environ.get
- forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '').split(',')
- forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',')
- forwarded_host = getter('HTTP_X_FORWARDED_HOST', '')
- forwarded_port = getter('HTTP_X_FORWARDED_PORT', '')
- forwarded_prefix = getter('HTTP_X_FORWARDED_PREFIX', '')
- environ.update({
- 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'),
- 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'),
- 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST'),
- 'werkzeug.proxy_fix.orig_server_port': getter('SERVER_PORT'),
- 'werkzeug.proxy_fix.orig_script_name': getter('SCRIPT_NAME'),
- })
- forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x]
- forwarded_proto = [x for x in [x.strip() for x in forwarded_proto] if x]
- remote_addr = self.get_remote_addr(forwarded_for)
- if remote_addr is not None:
- environ['REMOTE_ADDR'] = remote_addr
- if forwarded_host:
- environ['HTTP_HOST'] = forwarded_host
- if forwarded_port:
- if environ.get('HTTP_HOST'):
- parts = environ['HTTP_HOST'].split(':', 1)
- if len(parts) == 2:
- environ['HTTP_HOST'] = parts[0] + ':' + forwarded_port
+ environ['werkzeug.proxy_fix.orig'] = {
+ 'REMOTE_ADDR': getter('REMOTE_ADDR'),
+ 'wsgi.url_scheme': getter('wsgi.url_scheme'),
+ 'HTTP_HOST': getter('HTTP_HOST'),
+ 'SERVER_NAME': getter('SERVER_NAME'),
+ 'SERVER_PORT': getter('SERVER_PORT'),
+ 'SCRIPT_NAME': getter('SCRIPT_NAME'),
+ }
+
+ for get_key, offset_key, set_key in (
+ ('X_FORWARDED_FOR', 'x_for', 'REMOTE_ADDR'),
+ ('X_FORWARDED_PROTO', 'x_proto', 'wsgi.url_scheme'),
+ ('X_FORWARDED_HOST', 'x_host', self.set_x_host),
+ ('X_FORWARDED_PORT', 'x_port', self.set_x_port),
+ ('X_FORWARDED_PREFIX', 'x_prefix', 'SCRIPT_NAME'),
+ ):
+ value = self.get_by_offset(
+ offset_key,
+ [x.strip() for x in getter(get_key, '').split(',')])
+ if value:
+ if callable(set_key):
+ set_key(value, environ)
else:
- environ['HTTP_HOST'] += ':' + forwarded_port
- else:
- environ['SERVER_PORT'] = forwarded_port
- if forwarded_proto:
- environ['wsgi.url_scheme'] = forwarded_proto[0]
- if forwarded_prefix:
- environ['SCRIPT_NAME'] = forwarded_prefix
+ environ[set_key] = value
+
return self.app(environ, start_response)
diff --git a/werkzeug/wsgi.py b/werkzeug/wsgi.py
index ec7bb0a9..1f58c18b 100644
--- a/werkzeug/wsgi.py
+++ b/werkzeug/wsgi.py
@@ -143,17 +143,20 @@ def host_is_trusted(hostname, trusted_list):
def get_host(environ, trusted_hosts=None):
- """Return the real host for the given WSGI environment. This first checks
- the normal `Host` header, and if it's not present, then `SERVER_NAME`
- and `SERVER_PORT` environment variables.
-
- Optionally it verifies that the host is in a list of trusted hosts.
- If the host is not in there it will raise a
- :exc:`~werkzeug.exceptions.SecurityError`.
-
- :param environ: the WSGI environment to get the host of.
- :param trusted_hosts: a list of trusted hosts, see :func:`host_is_trusted`
- for more information.
+ """Return the host for the given WSGI environment. This first checks
+ the ``Host`` header. If it's not present, then ``SERVER_NAME`` and
+ ``SERVER_PORT`` are used. The host will only contain the port if it
+ is different than the standard port for the protocol.
+
+ Optionally, verify that the host is trusted using
+ :func:`host_is_trusted` and raise a
+ :exc:`~werkzeug.exceptions.SecurityError` if it is not.
+
+ :param environ: The WSGI environment to get the host from.
+ :param trusted_hosts: A list of trusted hosts.
+ :return: Host, with port if necessary.
+ :raise ~werkzeug.exceptions.SecurityError: If the host is not
+ trusted.
"""
if 'HTTP_HOST' in environ:
rv = environ['HTTP_HOST']