feat(Request): Add support for several proxy "forwarded" headers (#1083)
Add support for some "forwarded" headers, included several new attributes and a reworking of some of the existing code to better facilitate sharing and performance. Also clean up a couple tiny nits in the docstrings for the sake of consistency.
This commit is contained in:

committed by
John Vrbanac

parent
c50a89d428
commit
b51d4e9a35
21
CHANGES.rst
21
CHANGES.rst
@@ -1,3 +1,24 @@
|
|||||||
|
1.3.0
|
||||||
|
=====
|
||||||
|
|
||||||
|
Breaking Changes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
(None)
|
||||||
|
|
||||||
|
New & Improved
|
||||||
|
--------------
|
||||||
|
|
||||||
|
- A number of properties were added to ``falcon.Request`` to
|
||||||
|
expose information added by proxies in front of the application
|
||||||
|
server. These include the `forwarded`, `forwarded_uri`,
|
||||||
|
`forwarded_scheme`, `forwarded_host`, and `forwarded_prefix`
|
||||||
|
properties. The `prefix` attribute was also added as part of this
|
||||||
|
work.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
-----
|
||||||
|
|
||||||
1.2.0
|
1.2.0
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
@@ -23,6 +23,9 @@ Request
|
|||||||
.. autoclass:: falcon.Request
|
.. autoclass:: falcon.Request
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: falcon.Forwarded
|
||||||
|
:members:
|
||||||
|
|
||||||
Response
|
Response
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
20
docs/changes/1.3.0.rst
Normal file
20
docs/changes/1.3.0.rst
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Changelog for Falcon 1.3.0
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Breaking Changes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
(None)
|
||||||
|
|
||||||
|
New & Improved
|
||||||
|
--------------
|
||||||
|
|
||||||
|
- A number of attributes were added to :class:`falcon.Request` to
|
||||||
|
expose information added by proxies in front of the application
|
||||||
|
server. These include the `forwarded`, `forwarded_uri`,
|
||||||
|
`forwarded_scheme`, `forwarded_host`, and `forwarded_prefix`
|
||||||
|
attribute. The `prefix` attribute was also added as part of this
|
||||||
|
work.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
-----
|
@@ -3,6 +3,7 @@ Changelogs
|
|||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
|
1.3.0 <1.3.0>
|
||||||
1.2.0 <1.2.0>
|
1.2.0 <1.2.0>
|
||||||
1.1.0 <1.1.0>
|
1.1.0 <1.1.0>
|
||||||
1.0.0 <1.0.0>
|
1.0.0 <1.0.0>
|
||||||
|
@@ -54,5 +54,5 @@ import falcon.uri # NOQA
|
|||||||
from falcon.util import * # NOQA
|
from falcon.util import * # NOQA
|
||||||
|
|
||||||
from falcon.hooks import before, after # NOQA
|
from falcon.hooks import before, after # NOQA
|
||||||
from falcon.request import Request, RequestOptions # NOQA
|
from falcon.request import Request, RequestOptions, Forwarded # NOQA
|
||||||
from falcon.response import Response, ResponseOptions # NOQA
|
from falcon.response import Response, ResponseOptions # NOQA
|
||||||
|
@@ -7,7 +7,7 @@ import six
|
|||||||
class BaseHandler(object):
|
class BaseHandler(object):
|
||||||
"""Abstract Base Class for an internet media type handler"""
|
"""Abstract Base Class for an internet media type handler"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod # pragma: no cover
|
||||||
def serialize(self, obj):
|
def serialize(self, obj):
|
||||||
"""Serialize the media object on a :any:`falcon.Response`
|
"""Serialize the media object on a :any:`falcon.Response`
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class BaseHandler(object):
|
|||||||
bytes: The resulting serialized bytes from the input object.
|
bytes: The resulting serialized bytes from the input object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod # pragma: no cover
|
||||||
def deserialize(self, raw):
|
def deserialize(self, raw):
|
||||||
"""Deserialize the :any:`falcon.Request` body.
|
"""Deserialize the :any:`falcon.Request` body.
|
||||||
|
|
||||||
|
@@ -74,12 +74,59 @@ class Request(object):
|
|||||||
options (dict): Set of global options passed from the API handler.
|
options (dict): Set of global options passed from the API handler.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
scheme (str): Either 'http' or 'https'.
|
scheme (str): URL scheme used for the request. Either 'http' or
|
||||||
|
'https'.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the request was proxied, the scheme may not
|
||||||
|
match what was originally requested by the client.
|
||||||
|
:py:attr:`forwarded_scheme` can be used, instead,
|
||||||
|
to handle such cases.
|
||||||
|
|
||||||
|
forwarded_scheme (str): Original URL scheme requested by the
|
||||||
|
user agent, if the request was proxied. Typical values are
|
||||||
|
'http' or 'https'.
|
||||||
|
|
||||||
|
The following request headers are checked, in order of
|
||||||
|
preference, to determine the forwarded scheme:
|
||||||
|
|
||||||
|
- ``Forwarded``
|
||||||
|
- ``X-Forwarded-For``
|
||||||
|
|
||||||
|
If none of these headers are available, or if the
|
||||||
|
Forwarded header is available but does not contain a
|
||||||
|
"proto" parameter in the first hop, the value of
|
||||||
|
:attr:`scheme` is returned instead.
|
||||||
|
|
||||||
|
(See also: RFC 7239, Section 1)
|
||||||
|
|
||||||
protocol (str): Deprecated alias for `scheme`. Will be removed
|
protocol (str): Deprecated alias for `scheme`. Will be removed
|
||||||
in a future release.
|
in a future release.
|
||||||
method (str): HTTP method requested (e.g., 'GET', 'POST', etc.)
|
method (str): HTTP method requested (e.g., 'GET', 'POST', etc.)
|
||||||
host (str): Hostname requested by the client
|
host (str): Host request header field
|
||||||
port (int): Port used for the request. If the request URL does
|
forwarded_host (str): Original host request header as received
|
||||||
|
by the first proxy in front of the application server.
|
||||||
|
|
||||||
|
The following request headers are checked, in order of
|
||||||
|
preference, to determine the forwarded scheme:
|
||||||
|
|
||||||
|
- ``Forwarded``
|
||||||
|
- ``X-Forwarded-Host``
|
||||||
|
|
||||||
|
If none of the above headers are available, or if the
|
||||||
|
Forwarded header is available but the "host"
|
||||||
|
parameter is not included in the first hop, the value of
|
||||||
|
:attr:`host` is returned instead.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Reverse proxies are often configured to set the Host
|
||||||
|
header directly to the one that was originally
|
||||||
|
requested by the user agent; in that case, using
|
||||||
|
:attr:`host` is sufficient.
|
||||||
|
|
||||||
|
(See also: RFC 7239, Section 4)
|
||||||
|
|
||||||
|
port (int): Port used for the request. If the request URI does
|
||||||
not specify a port, the default one for the given schema is
|
not specify a port, the default one for the given schema is
|
||||||
returned (80 for HTTP and 443 for HTTPS).
|
returned (80 for HTTP and 443 for HTTPS).
|
||||||
netloc (str): Returns the 'host:port' portion of the request
|
netloc (str): Returns the 'host:port' portion of the request
|
||||||
@@ -94,9 +141,15 @@ class Request(object):
|
|||||||
for `subdomain` is undefined.
|
for `subdomain` is undefined.
|
||||||
|
|
||||||
env (dict): Reference to the WSGI environ ``dict`` passed in from the
|
env (dict): Reference to the WSGI environ ``dict`` passed in from the
|
||||||
server. See also PEP-3333.
|
server. (See also PEP-3333.)
|
||||||
app (str): Name of the WSGI app (if using WSGI's notion of virtual
|
app (str): The initial portion of the request URI's path that
|
||||||
hosting).
|
corresponds to the application object, so that the
|
||||||
|
application knows its virtual "location". This may be an
|
||||||
|
empty string, if the application corresponds to the "root"
|
||||||
|
of the server.
|
||||||
|
|
||||||
|
(Corresponds to the "SCRIPT_NAME" environ variable defined
|
||||||
|
by PEP-3333.)
|
||||||
access_route(list): IP address of the original client, as well
|
access_route(list): IP address of the original client, as well
|
||||||
as any known addresses of proxies fronting the WSGI server.
|
as any known addresses of proxies fronting the WSGI server.
|
||||||
|
|
||||||
@@ -152,15 +205,26 @@ class Request(object):
|
|||||||
the current Request instance. Therefore the first argument is
|
the current Request instance. Therefore the first argument is
|
||||||
the Request instance itself (self).
|
the Request instance itself (self).
|
||||||
uri (str): The fully-qualified URI for the request.
|
uri (str): The fully-qualified URI for the request.
|
||||||
url (str): alias for `uri`.
|
url (str): Alias for `uri`.
|
||||||
relative_uri (str): The path + query string portion of the full URI.
|
forwarded_uri (str): Original URI for proxied requests. Uses
|
||||||
path (str): Path portion of the request URL (not including query
|
:attr:`forwarded_scheme` and :attr:`forwarded_host` in
|
||||||
|
order to reconstruct the original URI requested by the user
|
||||||
|
agent.
|
||||||
|
relative_uri (str): The path and query string portion of the
|
||||||
|
request URI, omitting the scheme and host.
|
||||||
|
prefix (str): The prefix of the request URI, including scheme,
|
||||||
|
host, and WSGI app (if any).
|
||||||
|
forwarded_prefix (str): The prefix of the original URI for
|
||||||
|
proxied requests. Uses :attr:`forwarded_scheme` and
|
||||||
|
:attr:`forwarded_host` in order to reconstruct the
|
||||||
|
original URI.
|
||||||
|
path (str): Path portion of the request URI (not including query
|
||||||
string).
|
string).
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
`req.path` may be set to a new value by a `process_request()`
|
`req.path` may be set to a new value by a `process_request()`
|
||||||
middleware method in order to influence routing.
|
middleware method in order to influence routing.
|
||||||
query_string (str): Query string portion of the request URL, without
|
query_string (str): Query string portion of the request URI, without
|
||||||
the preceding '?' character.
|
the preceding '?' character.
|
||||||
uri_template (str): The template for the route that was matched for
|
uri_template (str): The template for the route that was matched for
|
||||||
this request. May be ``None`` if the request has not yet been
|
this request. May be ``None`` if the request has not yet been
|
||||||
@@ -168,7 +232,11 @@ class Request(object):
|
|||||||
methods. May also be ``None`` if your app uses a custom routing
|
methods. May also be ``None`` if your app uses a custom routing
|
||||||
engine and the engine does not provide the URI template when
|
engine and the engine does not provide the URI template when
|
||||||
resolving a route.
|
resolving a route.
|
||||||
|
forwarded (list): Value of the Forwarded header, as a parsed list
|
||||||
|
of :class:`falcon.Forwarded` objects, or ``None`` if the header
|
||||||
|
is missing.
|
||||||
|
|
||||||
|
(See also: RFC 7239, Section 4)
|
||||||
user_agent (str): Value of the User-Agent header, or ``None`` if the
|
user_agent (str): Value of the User-Agent header, or ``None`` if the
|
||||||
header is missing.
|
header is missing.
|
||||||
referer (str): Value of Referer header, or ``None`` if
|
referer (str): Value of Referer header, or ``None`` if
|
||||||
@@ -316,23 +384,27 @@ class Request(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
'__dict__',
|
||||||
|
'_bounded_stream',
|
||||||
|
'_cached_access_route',
|
||||||
|
'_cached_forwarded',
|
||||||
|
'_cached_forwarded_prefix',
|
||||||
|
'_cached_forwarded_uri',
|
||||||
'_cached_headers',
|
'_cached_headers',
|
||||||
'_cached_uri',
|
'_cached_prefix',
|
||||||
'_cached_relative_uri',
|
'_cached_relative_uri',
|
||||||
|
'_cached_uri',
|
||||||
|
'_cookies',
|
||||||
|
'_params',
|
||||||
|
'_wsgierrors',
|
||||||
'content_type',
|
'content_type',
|
||||||
|
'context',
|
||||||
'env',
|
'env',
|
||||||
'method',
|
'method',
|
||||||
'_params',
|
'options',
|
||||||
'path',
|
'path',
|
||||||
'query_string',
|
'query_string',
|
||||||
'stream',
|
'stream',
|
||||||
'_bounded_stream',
|
|
||||||
'context',
|
|
||||||
'_wsgierrors',
|
|
||||||
'options',
|
|
||||||
'_cookies',
|
|
||||||
'_cached_access_route',
|
|
||||||
'__dict__',
|
|
||||||
'uri_template',
|
'uri_template',
|
||||||
'_media',
|
'_media',
|
||||||
)
|
)
|
||||||
@@ -388,10 +460,14 @@ class Request(object):
|
|||||||
|
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
|
|
||||||
self._cached_headers = None
|
|
||||||
self._cached_uri = None
|
|
||||||
self._cached_relative_uri = None
|
|
||||||
self._cached_access_route = None
|
self._cached_access_route = None
|
||||||
|
self._cached_forwarded = None
|
||||||
|
self._cached_forwarded_prefix = None
|
||||||
|
self._cached_forwarded_uri = None
|
||||||
|
self._cached_headers = None
|
||||||
|
self._cached_prefix = None
|
||||||
|
self._cached_relative_uri = None
|
||||||
|
self._cached_uri = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.content_type = self.env['CONTENT_TYPE']
|
self.content_type = self.env['CONTENT_TYPE']
|
||||||
@@ -457,6 +533,70 @@ class Request(object):
|
|||||||
|
|
||||||
referer = helpers.header_property('HTTP_REFERER')
|
referer = helpers.header_property('HTTP_REFERER')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forwarded(self):
|
||||||
|
# PERF(kgriffs): We could DRY up this memoization pattern using
|
||||||
|
# a decorator, but that would incur additional overhead without
|
||||||
|
# resorting to some trickery to rewrite the body of the method
|
||||||
|
# itself (vs. simply wrapping it with some memoization logic).
|
||||||
|
# At some point we might look into this but I don't think
|
||||||
|
# it's worth it right now.
|
||||||
|
if self._cached_forwarded is None:
|
||||||
|
# PERF(kgriffs): If someone is calling this, they are probably
|
||||||
|
# confident that the header exists, so most of the time we
|
||||||
|
# expect this call to succeed. Therefore, we won't need to
|
||||||
|
# pay the penalty of a raised exception in most cases, and
|
||||||
|
# there is no need to spend extra cycles calling get() or
|
||||||
|
# checking beforehand whether the key is in the dict.
|
||||||
|
try:
|
||||||
|
forwarded = self.env['HTTP_FORWARDED']
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed_elements = []
|
||||||
|
|
||||||
|
for element in forwarded.split(','):
|
||||||
|
parsed_element = Forwarded()
|
||||||
|
|
||||||
|
# NOTE(kgriffs): Calling strip() is necessary here since
|
||||||
|
# "an HTTP list allows white spaces to occur between the
|
||||||
|
# identifiers" (see also RFC 7239, Section 7.1).
|
||||||
|
for param in element.strip().split(';'):
|
||||||
|
# PERF(kgriffs): partition() is faster than split().
|
||||||
|
name, __, value = param.partition('=')
|
||||||
|
if not value:
|
||||||
|
# NOTE(kgriffs): The '=' separator was not found or
|
||||||
|
# the value was missing. Ignore this malformed
|
||||||
|
# param.
|
||||||
|
continue
|
||||||
|
|
||||||
|
# NOTE(kgriffs): According to RFC 7239, parameter
|
||||||
|
# names are case-insensitive.
|
||||||
|
name = name.lower()
|
||||||
|
value = unquote_string(value)
|
||||||
|
if name == 'by':
|
||||||
|
parsed_element.dest = value
|
||||||
|
elif name == 'for':
|
||||||
|
parsed_element.src = value
|
||||||
|
elif name == 'host':
|
||||||
|
parsed_element.host = value
|
||||||
|
elif name == 'proto':
|
||||||
|
# NOTE(kgriffs): RFC 7239 only requires that
|
||||||
|
# the "proto" value conform to the Host ABNF
|
||||||
|
# described in RFC 7230. The Host ABNF, in turn,
|
||||||
|
# does not require that the scheme be in any
|
||||||
|
# particular case, so we normalize it here to be
|
||||||
|
# consistent with the WSGI spec that *does*
|
||||||
|
# require the value of 'wsgi.url_scheme' to be
|
||||||
|
# either 'http' or 'https' (case-sensitive).
|
||||||
|
parsed_element.scheme = value.lower()
|
||||||
|
|
||||||
|
parsed_elements.append(parsed_element)
|
||||||
|
|
||||||
|
self._cached_forwarded = parsed_elements
|
||||||
|
|
||||||
|
return self._cached_forwarded
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client_accepts_json(self):
|
def client_accepts_json(self):
|
||||||
return self.client_accepts('application/json')
|
return self.client_accepts('application/json')
|
||||||
@@ -579,30 +719,56 @@ class Request(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def app(self):
|
def app(self):
|
||||||
return self.env.get('SCRIPT_NAME', '')
|
# PERF(kgriffs): try..except is faster than get() assuming that
|
||||||
|
# we normally expect the key to exist. Even though PEP-3333
|
||||||
|
# allows WSGI servers to omit the key when the value is an
|
||||||
|
# empty string, uwsgi, gunicorn, waitress, and wsgiref all
|
||||||
|
# include it even in that case.
|
||||||
|
try:
|
||||||
|
return self.env['SCRIPT_NAME']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheme(self):
|
def scheme(self):
|
||||||
return self.env['wsgi.url_scheme']
|
return self.env['wsgi.url_scheme']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forwarded_scheme(self):
|
||||||
|
# PERF(kgriffs): Since the Forwarded header is still relatively
|
||||||
|
# new, we expect X-Forwarded-Proto to be more common, so
|
||||||
|
# try to avoid calling self.forwarded if we can, since it uses a
|
||||||
|
# try...catch that will usually result in a relatively expensive
|
||||||
|
# raised exception.
|
||||||
|
if 'HTTP_FORWARDED' in self.env:
|
||||||
|
first_hop = self.forwarded[0]
|
||||||
|
scheme = first_hop.scheme or self.scheme
|
||||||
|
else:
|
||||||
|
# PERF(kgriffs): This call should normally succeed, so
|
||||||
|
# just go for it without wasting time checking it
|
||||||
|
# first. Note also that the indexing operator is
|
||||||
|
# slightly faster than using get().
|
||||||
|
try:
|
||||||
|
scheme = self.env['HTTP_X_FORWARDED_PROTO'].lower()
|
||||||
|
except KeyError:
|
||||||
|
scheme = self.env['wsgi.url_scheme']
|
||||||
|
|
||||||
|
return scheme
|
||||||
|
|
||||||
# TODO(kgriffs): Remove this deprecated alias in Falcon 2.0
|
# TODO(kgriffs): Remove this deprecated alias in Falcon 2.0
|
||||||
protocol = scheme
|
protocol = scheme
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uri(self):
|
def uri(self):
|
||||||
if self._cached_uri is None:
|
if self._cached_uri is None:
|
||||||
protocol = self.env['wsgi.url_scheme']
|
scheme = self.env['wsgi.url_scheme']
|
||||||
|
|
||||||
# PERF: For small numbers of items, '+' is faster
|
# PERF: For small numbers of items, '+' is faster
|
||||||
# than ''.join(...). Concatenation is also generally
|
# than ''.join(...). Concatenation is also generally
|
||||||
# faster than formatting.
|
# faster than formatting.
|
||||||
value = (protocol + '://' +
|
value = (scheme + '://' +
|
||||||
self.netloc +
|
self.netloc +
|
||||||
self.app +
|
self.relative_uri)
|
||||||
self.path)
|
|
||||||
|
|
||||||
if self.query_string:
|
|
||||||
value = value + '?' + self.query_string
|
|
||||||
|
|
||||||
self._cached_uri = value
|
self._cached_uri = value
|
||||||
|
|
||||||
@@ -610,6 +776,53 @@ class Request(object):
|
|||||||
|
|
||||||
url = uri
|
url = uri
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forwarded_uri(self):
|
||||||
|
if self._cached_forwarded_uri is None:
|
||||||
|
# PERF: For small numbers of items, '+' is faster
|
||||||
|
# than ''.join(...). Concatenation is also generally
|
||||||
|
# faster than formatting.
|
||||||
|
value = (self.forwarded_scheme + '://' +
|
||||||
|
self.forwarded_host +
|
||||||
|
self.relative_uri)
|
||||||
|
|
||||||
|
self._cached_forwarded_uri = value
|
||||||
|
|
||||||
|
return self._cached_forwarded_uri
|
||||||
|
|
||||||
|
@property
|
||||||
|
def relative_uri(self):
|
||||||
|
if self._cached_relative_uri is None:
|
||||||
|
if self.query_string:
|
||||||
|
self._cached_relative_uri = (self.app + self.path + '?' +
|
||||||
|
self.query_string)
|
||||||
|
else:
|
||||||
|
self._cached_relative_uri = self.app + self.path
|
||||||
|
|
||||||
|
return self._cached_relative_uri
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefix(self):
|
||||||
|
if self._cached_prefix is None:
|
||||||
|
self._cached_prefix = (
|
||||||
|
self.env['wsgi.url_scheme'] + '://' +
|
||||||
|
self.netloc +
|
||||||
|
self.app
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cached_prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forwarded_prefix(self):
|
||||||
|
if self._cached_forwarded_prefix is None:
|
||||||
|
self._cached_forwarded_prefix = (
|
||||||
|
self.forwarded_scheme + '://' +
|
||||||
|
self.forwarded_host +
|
||||||
|
self.app
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cached_forwarded_prefix
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self):
|
||||||
try:
|
try:
|
||||||
@@ -625,23 +838,34 @@ class Request(object):
|
|||||||
|
|
||||||
return host
|
return host
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forwarded_host(self):
|
||||||
|
# PERF(kgriffs): Since the Forwarded header is still relatively
|
||||||
|
# new, we expect X-Forwarded-Host to be more common, so
|
||||||
|
# try to avoid calling self.forwarded if we can, since it uses a
|
||||||
|
# try...catch that will usually result in a relatively expensive
|
||||||
|
# raised exception.
|
||||||
|
if 'HTTP_FORWARDED' in self.env:
|
||||||
|
first_hop = self.forwarded[0]
|
||||||
|
host = first_hop.host or self.host
|
||||||
|
else:
|
||||||
|
# PERF(kgriffs): This call should normally succeed, assuming
|
||||||
|
# that the caller is expecting a forwarded header, so
|
||||||
|
# just go for it without wasting time checking it
|
||||||
|
# first.
|
||||||
|
try:
|
||||||
|
host = self.env['HTTP_X_FORWARDED_HOST']
|
||||||
|
except KeyError:
|
||||||
|
host = self.host
|
||||||
|
|
||||||
|
return host
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subdomain(self):
|
def subdomain(self):
|
||||||
# PERF(kgriffs): .partition is slightly faster than .split
|
# PERF(kgriffs): .partition is slightly faster than .split
|
||||||
subdomain, sep, remainder = self.host.partition('.')
|
subdomain, sep, remainder = self.host.partition('.')
|
||||||
return subdomain if sep else None
|
return subdomain if sep else None
|
||||||
|
|
||||||
@property
|
|
||||||
def relative_uri(self):
|
|
||||||
if self._cached_relative_uri is None:
|
|
||||||
if self.query_string:
|
|
||||||
self._cached_relative_uri = (self.app + self.path + '?' +
|
|
||||||
self.query_string)
|
|
||||||
else:
|
|
||||||
self._cached_relative_uri = self.app + self.path
|
|
||||||
|
|
||||||
return self._cached_relative_uri
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self):
|
def headers(self):
|
||||||
# NOTE(kgriffs: First time here will cache the dict so all we
|
# NOTE(kgriffs: First time here will cache the dict so all we
|
||||||
@@ -696,7 +920,11 @@ class Request(object):
|
|||||||
# aware that an upstream proxy is malfunctioning.
|
# aware that an upstream proxy is malfunctioning.
|
||||||
|
|
||||||
if 'HTTP_FORWARDED' in self.env:
|
if 'HTTP_FORWARDED' in self.env:
|
||||||
self._cached_access_route = self._parse_rfc_forwarded()
|
self._cached_access_route = []
|
||||||
|
for hop in self.forwarded:
|
||||||
|
if hop.src is not None:
|
||||||
|
host, __ = parse_host(hop.src)
|
||||||
|
self._cached_access_route.append(host)
|
||||||
elif 'HTTP_X_FORWARDED_FOR' in self.env:
|
elif 'HTTP_X_FORWARDED_FOR' in self.env:
|
||||||
addresses = self.env['HTTP_X_FORWARDED_FOR'].split(',')
|
addresses = self.env['HTTP_X_FORWARDED_FOR'].split(',')
|
||||||
self._cached_access_route = [ip.strip() for ip in addresses]
|
self._cached_access_route = [ip.strip() for ip in addresses]
|
||||||
@@ -738,7 +966,8 @@ class Request(object):
|
|||||||
# NOTE(kgriffs): According to PEP-3333 we should first
|
# NOTE(kgriffs): According to PEP-3333 we should first
|
||||||
# try to use the Host header if present.
|
# try to use the Host header if present.
|
||||||
#
|
#
|
||||||
# PERF(kgriffs): try..except is faster than .get
|
# PERF(kgriffs): try..except is faster than get() when we
|
||||||
|
# expect the key to be present most of the time.
|
||||||
try:
|
try:
|
||||||
netloc_value = env['HTTP_HOST']
|
netloc_value = env['HTTP_HOST']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -1383,33 +1612,6 @@ class Request(object):
|
|||||||
|
|
||||||
self._params.update(extra_params)
|
self._params.update(extra_params)
|
||||||
|
|
||||||
def _parse_rfc_forwarded(self):
|
|
||||||
"""Parse RFC 7239 "Forwarded" header.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: addresses derived from "for" parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
addr = []
|
|
||||||
|
|
||||||
for forwarded in self.env['HTTP_FORWARDED'].split(','):
|
|
||||||
for param in forwarded.split(';'):
|
|
||||||
# PERF(kgriffs): Partition() is faster than split().
|
|
||||||
key, _, val = param.strip().partition('=')
|
|
||||||
if not val:
|
|
||||||
# NOTE(kgriffs): The '=' separator was not found or
|
|
||||||
# it was, but the value was missing.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if key.lower() != 'for':
|
|
||||||
# We only want "for" params
|
|
||||||
continue
|
|
||||||
|
|
||||||
host, _ = parse_host(unquote_string(val))
|
|
||||||
addr.append(host)
|
|
||||||
|
|
||||||
return addr
|
|
||||||
|
|
||||||
|
|
||||||
# PERF: To avoid typos and improve storage space and speed over a dict.
|
# PERF: To avoid typos and improve storage space and speed over a dict.
|
||||||
class RequestOptions(object):
|
class RequestOptions(object):
|
||||||
@@ -1495,3 +1697,36 @@ class RequestOptions(object):
|
|||||||
self.strip_url_path_trailing_slash = True
|
self.strip_url_path_trailing_slash = True
|
||||||
self.default_media_type = DEFAULT_MEDIA_TYPE
|
self.default_media_type = DEFAULT_MEDIA_TYPE
|
||||||
self.media_handlers = Handlers()
|
self.media_handlers = Handlers()
|
||||||
|
|
||||||
|
|
||||||
|
class Forwarded(object):
|
||||||
|
"""Represents a parsed Forwarded header.
|
||||||
|
|
||||||
|
(See also: RFC 7239, Section 4)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
src (str): The value of the "for" parameter, or
|
||||||
|
``None`` if the parameter is absent. Identifies the
|
||||||
|
node making the request to the proxy.
|
||||||
|
dest (str): The value of the "by" parameter, or
|
||||||
|
``None`` if the parameter is absent. Identifies the
|
||||||
|
client-facing interface of the proxy.
|
||||||
|
host (str): The value of the "host" parameter, or
|
||||||
|
``None`` if the parameter is absent. Provides the host
|
||||||
|
request header field as received by the proxy.
|
||||||
|
scheme (str): The value of the "proto" parameter, or
|
||||||
|
``None`` if the parameter is absent. Indicates the
|
||||||
|
protocol that was used to make the request to
|
||||||
|
the proxy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE(kgriffs): Use "client" since "for" is a keyword, and
|
||||||
|
# "scheme" instead of "proto" to be consistent with the
|
||||||
|
# falcon.Request interface.
|
||||||
|
__slots__ = ('src', 'dest', 'host', 'scheme')
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.src = None
|
||||||
|
self.dest = None
|
||||||
|
self.host = None
|
||||||
|
self.scheme = None
|
||||||
|
@@ -157,6 +157,12 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1',
|
|||||||
else:
|
else:
|
||||||
port = str(port)
|
port = str(port)
|
||||||
|
|
||||||
|
# NOTE(kgriffs): Judging by the algorithm given in PEP-3333 for
|
||||||
|
# reconstructing the URL, SCRIPT_NAME is expected to contain a
|
||||||
|
# preceding slash character.
|
||||||
|
if app and not app.startswith('/'):
|
||||||
|
app = '/' + app
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
'SERVER_PROTOCOL': protocol,
|
'SERVER_PROTOCOL': protocol,
|
||||||
'SERVER_SOFTWARE': 'gunicorn/0.17.0',
|
'SERVER_SOFTWARE': 'gunicorn/0.17.0',
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import six
|
import six
|
||||||
@@ -11,7 +12,7 @@ import falcon.uri
|
|||||||
_PROTOCOLS = ['HTTP/1.0', 'HTTP/1.1']
|
_PROTOCOLS = ['HTTP/1.0', 'HTTP/1.1']
|
||||||
|
|
||||||
|
|
||||||
class TestReqVars(object):
|
class TestRequestAttributes(object):
|
||||||
|
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.qs = 'marker=deadbeef&limit=10'
|
self.qs = 'marker=deadbeef&limit=10'
|
||||||
@@ -105,10 +106,12 @@ class TestReqVars(object):
|
|||||||
path = req.path
|
path = req.path
|
||||||
query_string = req.query_string
|
query_string = req.query_string
|
||||||
|
|
||||||
expected_uri = ''.join([scheme, '://', host, app, path,
|
expected_prefix = ''.join([scheme, '://', host, app])
|
||||||
'?', query_string])
|
expected_uri = ''.join([expected_prefix, path, '?', query_string])
|
||||||
|
|
||||||
assert expected_uri == req.uri
|
assert req.uri == expected_uri
|
||||||
|
assert req.prefix == expected_prefix
|
||||||
|
assert req.prefix == expected_prefix # Check cached value
|
||||||
|
|
||||||
@pytest.mark.skipif(not six.PY3, reason='Test only applies to Python 3')
|
@pytest.mark.skipif(not six.PY3, reason='Test only applies to Python 3')
|
||||||
@pytest.mark.parametrize('test_path', [
|
@pytest.mark.parametrize('test_path', [
|
||||||
@@ -141,10 +144,11 @@ class TestReqVars(object):
|
|||||||
assert req.path == falcon.uri.decode(test_path)
|
assert req.path == falcon.uri.decode(test_path)
|
||||||
|
|
||||||
def test_uri(self):
|
def test_uri(self):
|
||||||
uri = ('http://' + testing.DEFAULT_HOST + ':8080' +
|
prefix = 'http://' + testing.DEFAULT_HOST + ':8080' + self.app
|
||||||
self.app + self.relative_uri)
|
uri = prefix + self.relative_uri
|
||||||
|
|
||||||
assert self.req.url == uri
|
assert self.req.url == uri
|
||||||
|
assert self.req.prefix == prefix
|
||||||
|
|
||||||
# NOTE(kgriffs): Call twice to check caching works
|
# NOTE(kgriffs): Call twice to check caching works
|
||||||
assert self.req.uri == uri
|
assert self.req.uri == uri
|
||||||
@@ -695,20 +699,35 @@ class TestReqVars(object):
|
|||||||
assert req.scheme == scheme
|
assert req.scheme == scheme
|
||||||
assert req.port == 443
|
assert req.port == 443
|
||||||
|
|
||||||
@pytest.mark.parametrize('protocol', _PROTOCOLS)
|
@pytest.mark.parametrize(
|
||||||
def test_scheme_http(self, protocol):
|
'protocol, set_forwarded_proto',
|
||||||
|
list(itertools.product(_PROTOCOLS, [True, False]))
|
||||||
|
)
|
||||||
|
def test_scheme_http(self, protocol, set_forwarded_proto):
|
||||||
scheme = 'http'
|
scheme = 'http'
|
||||||
|
forwarded_scheme = 'HttPs'
|
||||||
|
|
||||||
|
headers = dict(self.headers)
|
||||||
|
|
||||||
|
if set_forwarded_proto:
|
||||||
|
headers['X-Forwarded-Proto'] = forwarded_scheme
|
||||||
|
|
||||||
req = Request(testing.create_environ(
|
req = Request(testing.create_environ(
|
||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
scheme=scheme,
|
scheme=scheme,
|
||||||
app=self.app,
|
app=self.app,
|
||||||
path='/hello',
|
path='/hello',
|
||||||
query_string=self.qs,
|
query_string=self.qs,
|
||||||
headers=self.headers))
|
headers=headers))
|
||||||
|
|
||||||
assert req.scheme == scheme
|
assert req.scheme == scheme
|
||||||
assert req.port == 80
|
assert req.port == 80
|
||||||
|
|
||||||
|
if set_forwarded_proto:
|
||||||
|
assert req.forwarded_scheme == forwarded_scheme.lower()
|
||||||
|
else:
|
||||||
|
assert req.forwarded_scheme == scheme
|
||||||
|
|
||||||
@pytest.mark.parametrize('protocol', _PROTOCOLS)
|
@pytest.mark.parametrize('protocol', _PROTOCOLS)
|
||||||
def test_netloc_default_port(self, protocol):
|
def test_netloc_default_port(self, protocol):
|
||||||
req = Request(testing.create_environ(
|
req = Request(testing.create_environ(
|
||||||
@@ -750,6 +769,21 @@ class TestReqVars(object):
|
|||||||
assert req.port == port
|
assert req.port == port
|
||||||
assert req.netloc == '{0}:{1}'.format(host, port)
|
assert req.netloc == '{0}:{1}'.format(host, port)
|
||||||
|
|
||||||
|
def test_app_present(self):
|
||||||
|
req = Request(testing.create_environ(app='/moving-pictures'))
|
||||||
|
assert req.app == '/moving-pictures'
|
||||||
|
|
||||||
|
def test_app_blank(self):
|
||||||
|
req = Request(testing.create_environ(app=''))
|
||||||
|
assert req.app == ''
|
||||||
|
|
||||||
|
def test_app_missing(self):
|
||||||
|
env = testing.create_environ()
|
||||||
|
del env['SCRIPT_NAME']
|
||||||
|
req = Request(env)
|
||||||
|
|
||||||
|
assert req.app == ''
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
122
tests/test_request_forwarded.py
Normal file
122
tests/test_request_forwarded.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from falcon.request import Request
|
||||||
|
import falcon.testing as testing
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_forwarded_headers():
|
||||||
|
req = Request(testing.create_environ(
|
||||||
|
host='example.com',
|
||||||
|
path='/languages',
|
||||||
|
app='backoffice'
|
||||||
|
))
|
||||||
|
|
||||||
|
assert req.forwarded is None
|
||||||
|
assert req.forwarded_uri == req.uri
|
||||||
|
assert req.forwarded_uri == 'http://example.com/backoffice/languages'
|
||||||
|
assert req.forwarded_prefix == 'http://example.com/backoffice'
|
||||||
|
|
||||||
|
|
||||||
|
def test_x_forwarded_host():
|
||||||
|
req = Request(testing.create_environ(
|
||||||
|
host='suchproxy.suchtesting.com',
|
||||||
|
path='/languages',
|
||||||
|
headers={'X-Forwarded-Host': 'something.org'}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert req.forwarded is None
|
||||||
|
assert req.forwarded_host == 'something.org'
|
||||||
|
assert req.forwarded_uri != req.uri
|
||||||
|
assert req.forwarded_uri == 'http://something.org/languages'
|
||||||
|
assert req.forwarded_prefix == 'http://something.org'
|
||||||
|
assert req.forwarded_prefix == 'http://something.org' # Check cached value
|
||||||
|
|
||||||
|
|
||||||
|
def test_x_forwarded_proto():
|
||||||
|
req = Request(testing.create_environ(
|
||||||
|
host='example.org',
|
||||||
|
path='/languages',
|
||||||
|
headers={'X-Forwarded-Proto': 'HTTPS'}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert req.forwarded is None
|
||||||
|
assert req.forwarded_scheme == 'https'
|
||||||
|
assert req.forwarded_uri != req.uri
|
||||||
|
assert req.forwarded_uri == 'https://example.org/languages'
|
||||||
|
assert req.forwarded_prefix == 'https://example.org'
|
||||||
|
|
||||||
|
|
||||||
|
def test_forwarded_host():
|
||||||
|
req = Request(testing.create_environ(
|
||||||
|
host='suchproxy02.suchtesting.com',
|
||||||
|
path='/languages',
|
||||||
|
headers={
|
||||||
|
'Forwarded': 'host=something.org , host=suchproxy01.suchtesting.com'
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert req.forwarded is not None
|
||||||
|
for f in req.forwarded:
|
||||||
|
assert f.src is None
|
||||||
|
assert f.dest is None
|
||||||
|
assert f.scheme is None
|
||||||
|
|
||||||
|
assert req.forwarded[0].host == 'something.org'
|
||||||
|
assert req.forwarded[1].host == 'suchproxy01.suchtesting.com'
|
||||||
|
|
||||||
|
assert req.forwarded_host == 'something.org'
|
||||||
|
assert req.forwarded_uri != req.uri
|
||||||
|
assert req.forwarded_uri == 'http://something.org/languages'
|
||||||
|
assert req.forwarded_prefix == 'http://something.org'
|
||||||
|
|
||||||
|
|
||||||
|
def test_forwarded_multiple_params():
|
||||||
|
req = Request(testing.create_environ(
|
||||||
|
host='suchproxy02.suchtesting.com',
|
||||||
|
path='/languages',
|
||||||
|
headers={
|
||||||
|
'Forwarded': (
|
||||||
|
'host=something.org;proto=hTTps;ignore=me;for=108.166.30.185, '
|
||||||
|
'by=203.0.113.43;host=suchproxy01.suchtesting.com;proto=httP'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert req.forwarded is not None
|
||||||
|
|
||||||
|
assert req.forwarded[0].host == 'something.org'
|
||||||
|
assert req.forwarded[0].scheme == 'https'
|
||||||
|
assert req.forwarded[0].src == '108.166.30.185'
|
||||||
|
assert req.forwarded[0].dest is None
|
||||||
|
|
||||||
|
assert req.forwarded[1].host == 'suchproxy01.suchtesting.com'
|
||||||
|
assert req.forwarded[1].scheme == 'http'
|
||||||
|
assert req.forwarded[1].src is None
|
||||||
|
assert req.forwarded[1].dest == '203.0.113.43'
|
||||||
|
|
||||||
|
assert req.forwarded_scheme == 'https'
|
||||||
|
assert req.forwarded_host == 'something.org'
|
||||||
|
assert req.forwarded_uri != req.uri
|
||||||
|
assert req.forwarded_uri == 'https://something.org/languages'
|
||||||
|
assert req.forwarded_prefix == 'https://something.org'
|
||||||
|
|
||||||
|
|
||||||
|
def test_forwarded_missing_first_hop_host():
|
||||||
|
req = Request(testing.create_environ(
|
||||||
|
host='suchproxy02.suchtesting.com',
|
||||||
|
path='/languages',
|
||||||
|
app='doge',
|
||||||
|
headers={
|
||||||
|
'Forwarded': 'for=108.166.30.185,host=suchproxy01.suchtesting.com'
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert req.forwarded[0].host is None
|
||||||
|
assert req.forwarded[0].src == '108.166.30.185'
|
||||||
|
|
||||||
|
assert req.forwarded[1].host == 'suchproxy01.suchtesting.com'
|
||||||
|
assert req.forwarded[1].src is None
|
||||||
|
|
||||||
|
assert req.forwarded_scheme == 'http'
|
||||||
|
assert req.forwarded_host == 'suchproxy02.suchtesting.com'
|
||||||
|
assert req.forwarded_uri == req.uri
|
||||||
|
assert req.forwarded_uri == 'http://suchproxy02.suchtesting.com/doge/languages'
|
||||||
|
assert req.forwarded_prefix == 'http://suchproxy02.suchtesting.com/doge'
|
12
tox.ini
12
tox.ini
@@ -199,7 +199,17 @@ commands = uwsgi --http localhost:8000 --wsgi-file {toxinidir}/tests/dump_wsgi.p
|
|||||||
[testenv:py27_dump_gunicorn]
|
[testenv:py27_dump_gunicorn]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = gunicorn
|
deps = gunicorn
|
||||||
commands = gunicorn: gunicorn -b localhost:8000 tests.dump_wsgi
|
commands = gunicorn -b localhost:8000 tests.dump_wsgi
|
||||||
|
|
||||||
|
[testenv:py36_dump_gunicorn]
|
||||||
|
basepython = python3.6
|
||||||
|
deps = gunicorn
|
||||||
|
commands = gunicorn -b localhost:8000 tests.dump_wsgi
|
||||||
|
|
||||||
|
[testenv:py27_dump_waitress]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = waitress
|
||||||
|
commands = waitress-serve --listen=localhost:8000 tests.dump_wsgi:application
|
||||||
|
|
||||||
[testenv:py27_dump_wsgiref]
|
[testenv:py27_dump_wsgiref]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
|
Reference in New Issue
Block a user