Merge pull request #7 from channelcat/master

merge upstreaming master branch
This commit is contained in:
7 2017-07-22 18:28:38 -07:00 committed by GitHub
commit e27c7ba36f
5 changed files with 86 additions and 13 deletions

View File

@ -12,7 +12,7 @@ _address_dict = {
'Windows': ('localhost', 514), 'Windows': ('localhost', 514),
'Darwin': '/var/run/syslog', 'Darwin': '/var/run/syslog',
'Linux': '/dev/log', 'Linux': '/dev/log',
'FreeBSD': '/dev/log' 'FreeBSD': '/var/run/log'
} }
LOGGING = { LOGGING = {

View File

@ -209,25 +209,43 @@ class Unauthorized(SanicException):
Unauthorized exception (401 HTTP status code). Unauthorized exception (401 HTTP status code).
:param scheme: Name of the authentication scheme to be used. :param scheme: Name of the authentication scheme to be used.
:param realm: Description of the protected area. (optional)
:param challenge: A dict containing values to add to the WWW-Authenticate :param challenge: A dict containing values to add to the WWW-Authenticate
header that is generated. This is especially useful when header that is generated. This is especially useful when dealing with
dealing with the Digest scheme. (optional) the Digest scheme. (optional)
Examples::
# With a Basic auth-scheme, realm MUST be present:
challenge = {"realm": "Restricted Area"}
raise Unauthorized("Auth required.", "Basic", challenge)
# With a Digest auth-scheme, things are a bit more complicated:
challenge = {
"realm": "Restricted Area",
"qop": "auth, auth-int",
"algorithm": "MD5",
"nonce": "abcdef",
"opaque": "zyxwvu"
}
raise Unauthorized("Auth required.", "Digest", challenge)
# With a Bearer auth-scheme, realm is optional:
challenge = {"realm": "Restricted Area"}
raise Unauthorized("Auth required.", "Bearer", challenge)
""" """
pass pass
def __init__(self, message, scheme, realm="", challenge=None): def __init__(self, message, scheme, challenge=None):
super().__init__(message) super().__init__(message)
adds = "" chal = ""
if challenge is not None: if challenge is not None:
values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()]
adds = ', '.join(values) chal = ', '.join(values)
adds = ', {}'.format(adds)
self.headers = { self.headers = {
"WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds) "WWW-Authenticate": "{} {}".format(scheme, chal).rstrip()
} }

View File

@ -45,7 +45,7 @@ class Request(dict):
__slots__ = ( __slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport', 'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url', 'uri_template', 'stream' '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr'
) )
def __init__(self, url_bytes, headers, version, method, transport): def __init__(self, url_bytes, headers, version, method, transport):
@ -142,7 +142,7 @@ class Request(dict):
@property @property
def cookies(self): def cookies(self):
if self._cookies is None: if self._cookies is None:
cookie = self.headers.get('Cookie') or self.headers.get('cookie') cookie = self.headers.get('Cookie')
if cookie is not None: if cookie is not None:
cookies = SimpleCookie() cookies = SimpleCookie()
cookies.load(cookie) cookies.load(cookie)
@ -159,6 +159,25 @@ class Request(dict):
(None, None)) (None, None))
return self._ip return self._ip
@property
def remote_addr(self):
"""Attempt to return the original client ip based on X-Forwarded-For.
:return: original client ip.
"""
if not hasattr(self, '_remote_addr'):
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
remote_addrs = [
addr for addr in [
addr.strip() for addr in forwarded_for
] if addr
]
if len(remote_addrs) > 0:
self._remote_addr = remote_addrs[0]
else:
self._remote_addr = ''
return self._remote_addr
@property @property
def scheme(self): def scheme(self):
if self.app.websocket_enabled \ if self.app.websocket_enabled \

View File

@ -33,17 +33,22 @@ def exception_app():
@app.route('/401/basic') @app.route('/401/basic')
def handler_401_basic(request): def handler_401_basic(request):
raise Unauthorized("Unauthorized", "Basic", "Sanic") raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"})
@app.route('/401/digest') @app.route('/401/digest')
def handler_401_digest(request): def handler_401_digest(request):
challenge = { challenge = {
"realm": "Sanic",
"qop": "auth, auth-int", "qop": "auth, auth-int",
"algorithm": "MD5", "algorithm": "MD5",
"nonce": "abcdef", "nonce": "abcdef",
"opaque": "zyxwvu", "opaque": "zyxwvu",
} }
raise Unauthorized("Unauthorized", "Digest", "Sanic", challenge) raise Unauthorized("Unauthorized", "Digest", challenge)
@app.route('/401/bearer')
def handler_401_bearer(request):
raise Unauthorized("Unauthorized", "Bearer")
@app.route('/invalid') @app.route('/invalid')
def handler_invalid(request): def handler_invalid(request):
@ -136,6 +141,10 @@ def test_unauthorized_exception(exception_app):
assert "nonce='abcdef'" in auth_header assert "nonce='abcdef'" in auth_header
assert "opaque='zyxwvu'" in auth_header assert "opaque='zyxwvu'" in auth_header
request, response = exception_app.test_client.get('/401/bearer')
assert response.status == 401
assert response.headers.get('WWW-Authenticate') == "Bearer"
def test_handled_unhandled_exception(exception_app): def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled""" """Test that an exception not built into sanic is handled"""

View File

@ -211,6 +211,32 @@ def test_content_type():
assert response.text == 'application/json' assert response.text == 'application/json'
def test_remote_addr():
app = Sanic('test_content_type')
@app.route('/')
async def handler(request):
return text(request.remote_addr)
headers = {
'X-Forwarded-For': '127.0.0.1, 127.0.1.2'
}
request, response = app.test_client.get('/', headers=headers)
assert request.remote_addr == '127.0.0.1'
assert response.text == '127.0.0.1'
request, response = app.test_client.get('/')
assert request.remote_addr == ''
assert response.text == ''
headers = {
'X-Forwarded-For': '127.0.0.1, , ,,127.0.1.2'
}
request, response = app.test_client.get('/', headers=headers)
assert request.remote_addr == '127.0.0.1'
assert response.text == '127.0.0.1'
def test_match_info(): def test_match_info():
app = Sanic('test_match_info') app = Sanic('test_match_info')
@ -259,6 +285,7 @@ def test_post_form_urlencoded():
assert request.form.get('test') == 'OK' assert request.form.get('test') == 'OK'
@pytest.mark.parametrize( @pytest.mark.parametrize(
'payload', [ 'payload', [
'------sanic\r\n' \ '------sanic\r\n' \