-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Changes from 10 commits
b1043b4
85fcdfc
be95cb4
9554ce0
3799f85
76b1624
1f79171
c2848ed
6d37b83
73d4121
0cb4b22
eee65a4
e5c7bfe
2b33c7d
15462c2
b672b16
105c9ed
145f31f
b2a0cc5
09cd914
df470cd
5560bab
ef0c597
cd643a7
d1ba86b
8ede7d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
import datetime | ||
import json | ||
import re | ||
import string | ||
import tempfile | ||
import warnings | ||
from email.utils import parsedate | ||
|
@@ -21,6 +22,35 @@ | |
FileField = collections.namedtuple( | ||
'Field', 'name filename file content_type headers') | ||
|
||
_TCHAR = string.digits + string.ascii_letters + r"!#$%&'*+\-.^_`|~" | ||
# notice the escape of '-' to prevent interpretation as range | ||
|
||
_TOKEN = r'[{tchar}]*'.format(tchar=_TCHAR) | ||
|
||
_QDTEXT = r'[{}]'.format( | ||
r''.join(chr(c) for c in (0x09, 0x20, 0x21, *range(0x23, 0x7F)))) | ||
# qdtext includes 0x5C to escape 0x5D ('\]') | ||
# qdtext excludes obs-text (because obsoleted, and encoding not specified) | ||
|
||
_QUOTED_PAIR = r'\\[\t {tchar}]'.format(tchar=_TCHAR) | ||
|
||
_QUOTED_STRING = r'"(?:{quoted_pair}|{qdtext})*"'.format( | ||
qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR) | ||
|
||
_FORWARDED_PARAMS = ( | ||
r'[bB][yY]|[fF][oO][rR]|[hH][oO][sS][tT]|[pP][rR][oO][tT][oO]') | ||
|
||
_FORWARDED_PAIR = ( | ||
r'^ *({forwarded_params})=({token}|{quoted_string}) *$'.format( | ||
forwarded_params=_FORWARDED_PARAMS, | ||
token=_TOKEN, | ||
quoted_string=_QUOTED_STRING)) | ||
# allow whitespace as specified in RFC 7239 section 7.1 | ||
|
||
_QUOTED_PAIR_REPLACE_RE = re.compile(r'\\([\t {tchar}])'.format(tchar=_TCHAR)) | ||
# same pattern as _QUOTED_PAIR but contains a capture group | ||
|
||
_FORWARDED_PAIR_RE = re.compile(_FORWARDED_PAIR) | ||
|
||
############################################################ | ||
# HTTP Request | ||
|
@@ -150,16 +180,61 @@ def secure(self): | |
""" | ||
return self.url.scheme == 'https' | ||
|
||
@reify | ||
def forwarded(self): | ||
""" A frozendict containing parsed Forwarded header(s). | ||
|
||
Makes an effort to parse Forwarded headers as specified by RFC 7239: | ||
|
||
- It adds all parameters (by, for, host, proto) in the order it finds | ||
them; starting at the topmost / first added 'Forwarded' header, at | ||
the leftmost / first-added parwameter. | ||
- It checks that the value has valid syntax in general as specified in | ||
section 4: either a 'token' or a 'quoted-string'. | ||
- It un-escapes found escape sequences. | ||
- It does NOT validate 'by' and 'for' contents as specified in section | ||
6. | ||
- It does NOT validate 'host' contents (Host ABNF). | ||
- It does NOT validate 'proto' contents for valid URI scheme names. | ||
|
||
Returns a dict(by=tuple(...), for=tuple(...), host=tuple(...), | ||
proto=tuple(...), ) | ||
""" | ||
params = {'by': [], 'for': [], 'host': [], 'proto': []} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the property's type should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
if hdrs.FORWARDED in self._message.headers: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop the check, |
||
for forwarded_elm in self._message.headers.getall(hdrs.FORWARDED): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop previous line, |
||
if len(forwarded_elm): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop this check, iteration over empty |
||
forwarded_pairs = tuple( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't create a tuple but make a generator comprehension. It saves both time and memory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please support
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is supported by the regexes and I've included tests for it |
||
_FORWARDED_PAIR_RE.findall(pair) | ||
for pair in forwarded_elm.split(';')) | ||
for forwarded_pair in forwarded_pairs: | ||
if len(forwarded_pair) != 1: | ||
# non-compliant syntax, ignore | ||
continue | ||
param = forwarded_pair[0][0].lower() | ||
value = forwarded_pair[0][1] | ||
if len(value) and value[0] == '"': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If empty value allowed? I mean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no need to check for a closing quote: the regex validates A |
||
# quoted string: replace quotes and escape | ||
# sequences | ||
value = _QUOTED_PAIR_REPLACE_RE.sub( | ||
r'\1', value[1:-1]) | ||
params[param].append(value) | ||
return params | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return |
||
|
||
@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']: | ||
proto = self.forwarded['proto'][0] | ||
elif hdrs.X_FORWARDED_PROTO in self._message.headers: | ||
proto = self._message.headers[hdrs.X_FORWARDED_PROTO] | ||
return proto | ||
|
||
@property | ||
def method(self): | ||
|
@@ -179,16 +254,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']: | ||
host = self.forwarded['host'][0] | ||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC spaces around
=
are allowed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure but as far as I can see that's not the case. Section 4 only says
forwarded-pair = token "=" value
and section 7 recallsNote that an HTTP list allows white spaces to occur between the identifiers, and the list may be split over multiple header fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO it's rare case but still allowed