Added support for HTTP Forwarded header and combined parsing of other proxy headers.
- Accessible via request.forwarded that tries parse_forwarded and then parse_xforwarded - parse_forwarded uses the Forwarded header, if config.FORWARDED_SECRET is provided and a matching header field is found - parse_xforwarded uses X-Real-IP and X-Forwarded-* much alike the existing implementation - This commit does not change existing request properties that still use the old code and won't make use of Forwarded headers.
This commit is contained in:
parent
84b41123f2
commit
f69383db04
@ -26,6 +26,7 @@ DEFAULT_CONFIG = {
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
"FORWARDED_SECRET": None,
|
||||
"PROXIES_COUNT": -1,
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"REAL_IP_HEADER": "X-Real-IP",
|
||||
|
50
sanic/forwarded.py
Normal file
50
sanic/forwarded.py
Normal file
@ -0,0 +1,50 @@
|
||||
import re
|
||||
|
||||
# https://tools.ietf.org/html/rfc7230#section-3.2.6 and https://tools.ietf.org/html/rfc7239#section-4
|
||||
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||
# _forwarded_pair = re.compile(f'(^|[;,])\\s*{_token}=(?:{_token}|{_quoted})', re.ASCII)
|
||||
# Same as forwarded_pair but with reversed string because that allows fast matching from end
|
||||
_forwarded_reverse = re.compile(f'(?:{_token}|{_quoted})={_token}\\s*($|[;,])', re.ASCII)
|
||||
|
||||
def parse_forwarded(header, secret=None):
|
||||
"""Parse HTTP Forwarded header.
|
||||
Last proxy is returned, or if a secret is provided, a proxy with secret="yoursecret".
|
||||
:return: dict with fields from matched element
|
||||
"""
|
||||
if header is None or secret is not None and secret not in header:
|
||||
return None
|
||||
ret = {}
|
||||
for m in _forwarded_reverse.finditer(header[::-1]):
|
||||
val_quoted, val_token, key, sep = m.groups()
|
||||
key, val = key.lower()[::-1], (val_token or val_quoted)[::-1]
|
||||
if key != 'secret': ret[key] = val
|
||||
elif val == secret: secret = None
|
||||
if sep == ";": continue
|
||||
if secret is None: break
|
||||
ret = {} # Advancing to previous comma-separated element
|
||||
return ret if secret is None and ret else None
|
||||
|
||||
def parse_xforwarded(headers, config):
|
||||
"""Parse X-Real-IP and X-Forwarded-* headers."""
|
||||
addr = config.REAL_IP_HEADER and headers.get(config.REAL_IP_HEADER)
|
||||
forwarded_for = config.FORWARDED_FOR_HEADER and headers.get(config.FORWARDED_FOR_HEADER)
|
||||
if not addr and forwarded_for:
|
||||
proxies_count = config.PROXIES_COUNT
|
||||
assert proxies_count == -1 or proxies_count > 0, config.PROXIES_COUNT
|
||||
try:
|
||||
proxies = forwarded_for.split(",")
|
||||
addr = proxies[-proxies_count] if proxies_count > 0 else proxies[0]
|
||||
except (AttributeError, IndexError):
|
||||
return None
|
||||
addr = addr.strip()
|
||||
if not addr:
|
||||
return None
|
||||
other = ((h, headers.get(f'x-forwarded-{h}')) for h in ('proto', 'host', 'port', 'path'))
|
||||
return {'for': addr, **{h: v for h, v in other if v}}
|
||||
|
||||
def test_parse_forwarded():
|
||||
example_header = 'secret=first;for=1.2.3.4, for=injected,host=", for=23.45.67.89;proto=https;host=site.tld;path="/app";secret=egah2CGj55fSJFs, for="[::1]";port=8000'
|
||||
assert parse_forwarded(example_header) == {'for': '[::1]', 'port': '8000'}
|
||||
assert parse_forwarded(example_header, secret='non-exist') == None
|
||||
assert parse_forwarded(example_header, secret='first') == {'for': '1.2.3.4'}
|
||||
assert parse_forwarded(example_header, secret='egah2CGj55fSJFs') == {'for': '23.45.67.89', 'proto': 'https', 'host': 'site.tld', 'path': '/app'}
|
@ -12,6 +12,7 @@ from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||
from httptools import parse_url
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.forwarded import parse_forwarded, parse_xforwarded
|
||||
from sanic.log import error_logger, logger
|
||||
|
||||
|
||||
@ -87,6 +88,7 @@ class Request(dict):
|
||||
"parsed_files",
|
||||
"parsed_form",
|
||||
"parsed_json",
|
||||
"parsed_forwarded",
|
||||
"raw_url",
|
||||
"stream",
|
||||
"transport",
|
||||
@ -107,6 +109,7 @@ class Request(dict):
|
||||
|
||||
# Init but do not inhale
|
||||
self.body_init()
|
||||
self.parsed_forwarded = None
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
@ -373,6 +376,15 @@ class Request(dict):
|
||||
or self.host.split(":")[0]
|
||||
)
|
||||
|
||||
@property
|
||||
def forwarded(self):
|
||||
if self.parsed_forwarded is None:
|
||||
self.parsed_forwarded = (
|
||||
parse_forwarded(self.headers.get('forwarded'), self.app.config.FORWARDED_SECRET) or
|
||||
parse_xforwarded(self.headers, self.app.config)
|
||||
)
|
||||
return self.parsed_forwarded
|
||||
|
||||
@property
|
||||
def server_port(self):
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user