diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 0febd3f08b..f0055a9496 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -428,6 +428,7 @@ DEFAULT_INDEX_TABLESPACE = '' X_FRAME_OPTIONS = 'SAMEORIGIN' USE_X_FORWARDED_HOST = False +USE_X_FORWARDED_PORT = False # The Python dotted path to the WSGI application that Django's internal server # (runserver) will use. If `None`, the return value of diff --git a/django/http/request.py b/django/http/request.py index 3f23077565..938d833551 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -79,7 +79,7 @@ class HttpRequest(object): else: # Reconstruct the host using the algorithm from PEP 333. host = self.META['SERVER_NAME'] - server_port = str(self.META['SERVER_PORT']) + server_port = self.get_port() if server_port != ('443' if self.is_secure() else '80'): host = '%s:%s' % (host, server_port) @@ -98,6 +98,14 @@ class HttpRequest(object): msg += " The domain name provided is not valid according to RFC 1034/1035." raise DisallowedHost(msg) + def get_port(self): + """Return the port number for the request as a string.""" + if settings.USE_X_FORWARDED_PORT and 'HTTP_X_FORWARDED_PORT' in self.META: + port = self.META['HTTP_X_FORWARDED_PORT'] + else: + port = self.META['SERVER_PORT'] + return str(port) + def get_full_path(self, force_append_slash=False): # RFC 3986 requires query string arguments to be in the ASCII range. # Rather than crash if this doesn't happen, we encode defensively. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 3f84d0d144..06f4d1dc4a 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -254,6 +254,14 @@ Methods :class:`~django.middleware.common.CommonMiddleware` or :class:`~django.middleware.csrf.CsrfViewMiddleware`. +.. method:: HttpRequest.get_port() + + .. versionadded:: 1.9 + + Returns the originating port of the request using information from the + ``HTTP_X_FORWARDED_PORT`` (if :setting:`USE_X_FORWARDED_PORT` is enabled) + and ``SERVER_PORT`` ``META`` variables, in that order. + .. method:: HttpRequest.get_full_path() Returns the ``path``, plus an appended query string, if applicable. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 0cd83615e7..1077b910f2 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2621,6 +2621,19 @@ A boolean that specifies whether to use the X-Forwarded-Host header in preference to the Host header. This should only be enabled if a proxy which sets this header is in use. +.. setting:: USE_X_FORWARDED_PORT + +USE_X_FORWARDED_PORT +-------------------- + +.. versionadded:: 1.9 + +Default: ``False`` + +A boolean that specifies whether to use the X-Forwarded-Port header in +preference to the ``SERVER_PORT`` ``META`` variable. This should only be +enabled if a proxy which sets this header is in use. + .. setting:: WSGI_APPLICATION WSGI_APPLICATION @@ -3329,6 +3342,7 @@ HTTP * :setting:`SIGNING_BACKEND` * :setting:`USE_ETAGS` * :setting:`USE_X_FORWARDED_HOST` +* :setting:`USE_X_FORWARDED_PORT` * :setting:`WSGI_APPLICATION` Logging diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index f016a08c32..6f2611f267 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -538,6 +538,9 @@ Requests and Responses returning an :class:`~django.http.HttpResponseForbidden` so that :data:`~django.conf.urls.handler403` is invoked. +* Added :meth:`HttpRequest.get_port() ` to + fetch the originating port of the request. + Tests ^^^^^ diff --git a/tests/requests/tests.py b/tests/requests/tests.py index bbf19df4ba..8a0b32d1c0 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -651,6 +651,38 @@ class HostValidationTests(SimpleTestCase): } request.get_host() + @override_settings(USE_X_FORWARDED_PORT=False) + def test_get_port(self): + request = HttpRequest() + request.META = { + 'SERVER_PORT': '8080', + 'HTTP_X_FORWARDED_PORT': '80', + } + # Shouldn't use the X-Forwarded-Port header + self.assertEqual(request.get_port(), '8080') + + request = HttpRequest() + request.META = { + 'SERVER_PORT': '8080', + } + self.assertEqual(request.get_port(), '8080') + + @override_settings(USE_X_FORWARDED_PORT=True) + def test_get_port_with_x_forwarded_port(self): + request = HttpRequest() + request.META = { + 'SERVER_PORT': '8080', + 'HTTP_X_FORWARDED_PORT': '80', + } + # Should use the X-Forwarded-Port header + self.assertEqual(request.get_port(), '80') + + request = HttpRequest() + request.META = { + 'SERVER_PORT': '8080', + } + self.assertEqual(request.get_port(), '8080') + @override_settings(DEBUG=True, ALLOWED_HOSTS=[]) def test_host_validation_disabled_in_debug_mode(self): """If ALLOWED_HOSTS is empty and DEBUG is True, all hosts pass."""