Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Forwarded, X-Forwarded-Scheme and X-Forwarded-Host for better scheme and host resolution #1881

Merged
merged 26 commits into from
May 22, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Changes

- Do not unquote `+` in match_info values #1816

- Use Forwarded, X-Forwarded-Scheme and X-Forwarded-Host for better scheme and host resolution. #1134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8 here. Please split this to 2 lines or more of up to 79 characters per line. For more information on PEP8 see the www.python.org page on PEP8.


- Fix sub-application middlewares resolution order #1853


Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Enrique Saez
Erich Healy
Eugene Chernyshov
Eugene Naydenov
Evert Lammerts
Frederik Gladhorn
Frederik Peter Aalund
Gabriel Tremblay
Expand Down
3 changes: 3 additions & 0 deletions aiohttp/hdrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
ETAG = istr('ETAG')
EXPECT = istr('EXPECT')
EXPIRES = istr('EXPIRES')
FORWARDED = istr('FORWARDED')
FROM = istr('FROM')
HOST = istr('HOST')
IF_MATCH = istr('IF-MATCH')
Expand Down Expand Up @@ -89,3 +90,5 @@
WANT_DIGEST = istr('WANT-DIGEST')
WARNING = istr('WARNING')
WWW_AUTHENTICATE = istr('WWW-AUTHENTICATE')
X_FORWARDED_HOST = istr('X-FORWARDED-HOST')
X_FORWARDED_PROTO = istr('X-FORWARDED-PROTO')
48 changes: 38 additions & 10 deletions aiohttp/web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,31 @@ def secure(self):
"""
return self.url.scheme == 'https'

@reify
def _forwarded(self):
forwarded = self._message.headers.get(hdrs.FORWARDED)
if forwarded is not None:
parsed = re.findall(
r'^by=([^;]*); +for=([^;]*); +host=([^;]*); +proto=(https?)$',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the parser should be more complicated.

The header field value can be
   defined in ABNF syntax as:

       Forwarded   = 1#forwarded-element

       forwarded-element =
           [ forwarded-pair ] *( ";" [ forwarded-pair ] )

       forwarded-pair = token "=" value
       value          = token / quoted-string

       token = <Defined in [RFC7230], Section 3.2.6>
       quoted-string = <Defined in [RFC7230], Section 3.2.6>

It means the forwarded-pair elements could be in random order: by=a; host=b.com and host=b.com, by=a are both valid combinations.
Also value could be optionally quoted, aiohttp shoud unquote it automatically.
pair-name is case-insensitive: all 'Host', 'host', and 'HOst` are valid names:

       Forwarded: for="_gazonk"
       Forwarded: For="[2001:db8:cafe::17]:4711"
       Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
       Forwarded: for=192.0.2.43, for=198.51.100.17

   Note that as ":" and "[]" are not valid characters in "token", IPv6
   addresses are written as "quoted-string".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note: the lase example contains two for pairs, it's not supported by your regexp now.

forwarded)
if parsed:
return parsed[0]
return None

@reify
def _scheme(self):
proto = 'http'
if self._transport.get_extra_info('sslcontext'):
return 'https'
secure_proxy_ssl_header = self._secure_proxy_ssl_header
if secure_proxy_ssl_header is not None:
header, value = secure_proxy_ssl_header
proto = 'https'
elif self._secure_proxy_ssl_header is not None:
header, value = self._secure_proxy_ssl_header
if self.headers.get(header) == value:
return 'https'
return 'http'
proto = 'https'
elif self._forwarded:
_, _, _, proto = self._forwarded
elif hdrs.X_FORWARDED_PROTO in self._message.headers:
proto = self._message.headers[hdrs.X_FORWARDED_PROTO]
return proto

@property
def method(self):
Expand All @@ -179,16 +194,29 @@ def version(self):

@reify
def host(self):
"""Read only property for getting *HOST* header of request.
""" Hostname of the request.

Hostname is resolved through the following headers, in this order:

- Forwarded
- X-Forwarded-Host
- Host

Returns str or None if HTTP request has no HOST header.
Returns str, or None if no hostname is found in the headers.
"""
return self._message.headers.get(hdrs.HOST)
host = None
if self._forwarded:
_, _, host, _ = self._forwarded
elif hdrs.X_FORWARDED_HOST in self._message.headers:
host = self._message.headers[hdrs.X_FORWARDED_HOST]
elif hdrs.HOST in self._message.headers:
host = self._message.headers[hdrs.HOST]
return host

@reify
def url(self):
return URL('{}://{}{}'.format(self._scheme,
self._message.headers.get(hdrs.HOST),
self.host,
str(self._rel_url)))

@property
Expand Down
56 changes: 56 additions & 0 deletions tests/test_web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,62 @@ def test_https_scheme_by_secure_proxy_ssl_header_false_test(make_request):
assert req.secure is False


def test_https_scheme_by_forwarded_header(make_request):
req = make_request('GET', '/',
headers=CIMultiDict(
{'Forwarded': 'by=; for=; host=; proto=https'}))
assert "https" == req.scheme
assert req.secure is True


def test_https_scheme_by_malformed_forwarded_header(make_request):
req = make_request('GET', '/',
headers=CIMultiDict({'Forwarded': 'malformed value'}))
assert "http" == req.scheme
assert req.secure is False


def test_https_scheme_by_x_forwarded_proto_header(make_request):
req = make_request('GET', '/',
headers=CIMultiDict({'X-Forwarded-Proto': 'https'}))
assert "https" == req.scheme
assert req.secure is True


def test_https_scheme_by_x_forwarded_proto_header_no_tls(make_request):
req = make_request('GET', '/',
headers=CIMultiDict({'X-Forwarded-Proto': 'http'}))
assert "http" == req.scheme
assert req.secure is False


def test_host_by_forwarded_header(make_request):
req = make_request('GET', '/',
headers=CIMultiDict(
{'Forwarded': 'by=; for=; host'
'=example.com; proto=https'}))
assert req.host == 'example.com'


def test_host_by_forwarded_header_malformed(make_request):
req = make_request('GET', '/',
headers=CIMultiDict({'Forwarded': 'malformed value'}))
assert req.host is None


def test_host_by_x_forwarded_host_header(make_request):
req = make_request('GET', '/',
headers=CIMultiDict(
{'X-Forwarded-Host': 'example.com'}))
assert req.host == 'example.com'


def test_host_by_host_header(make_request):
req = make_request('GET', '/',
headers=CIMultiDict({'Host': 'example.com'}))
assert req.host == 'example.com'


def test_raw_headers(make_request):
req = make_request('GET', '/',
headers=CIMultiDict({'X-HEADER': 'aaa'}))
Expand Down