From e427e38da82372974c174843f91028c7b1053e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Thu, 29 Jun 2017 12:34:52 +0200 Subject: [PATCH 1/4] Simplified the `Unauthorized.__init__` signature. It doesn't really make sense to have a `realm` parameter in the method signature. Instead, one can simply set the realm in the `challenge` dict if necessary. Also fixed the tests accordingly (and added a new one for "Bearer" auth-scheme). --- sanic/exceptions.py | 31 +++++++++++++++++++++++++------ tests/test_exceptions.py | 13 +++++++++++-- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 95e41b4a..d05342fa 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -204,25 +204,44 @@ class Unauthorized(SanicException): Unauthorized exception (401 HTTP status code). :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 header that is generated. This is especially useful when dealing with 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 - def __init__(self, message, scheme, realm="", challenge=None): + def __init__(self, message, scheme, challenge=None): super().__init__(message) - adds = "" + chal = "" if challenge is not None: values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] - adds = ', '.join(values) - adds = ', {}'.format(adds) + chal = ', '.join(values) self.headers = { - "WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds) + "WWW-Authenticate": "{} {}".format(scheme, chal).rstrip() } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index dcdecabd..db1fc246 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -29,17 +29,22 @@ def exception_app(): @app.route('/401/basic') def handler_401_basic(request): - raise Unauthorized("Unauthorized", "Basic", "Sanic") + raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"}) @app.route('/401/digest') def handler_401_digest(request): challenge = { + "realm": "Sanic", "qop": "auth, auth-int", "algorithm": "MD5", "nonce": "abcdef", "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') def handler_invalid(request): @@ -126,6 +131,10 @@ def test_unauthorized_exception(exception_app): assert "nonce='abcdef'" 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): """Test that an exception not built into sanic is handled""" From 75378d3567489fa3318de282dfe7b13df205ded2 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 14 Jul 2017 09:29:16 -0700 Subject: [PATCH 2/4] add remote_addr property for proxy fix --- sanic/request.py | 21 +++++++++++++++++++-- tests/test_requests.py | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index d8674c48..eb05f471 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -45,7 +45,7 @@ class Request(dict): __slots__ = ( 'app', 'headers', 'version', 'method', '_cookies', 'transport', '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): @@ -142,7 +142,7 @@ class Request(dict): @property def cookies(self): 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: cookies = SimpleCookie() cookies.load(cookie) @@ -159,6 +159,23 @@ class Request(dict): (None, None)) 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 def scheme(self): if self.app.websocket_enabled \ diff --git a/tests/test_requests.py b/tests/test_requests.py index 997f32cb..f0696c7f 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -211,6 +211,32 @@ def test_content_type(): 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(): app = Sanic('test_match_info') @@ -259,6 +285,7 @@ def test_post_form_urlencoded(): assert request.form.get('test') == 'OK' + @pytest.mark.parametrize( 'payload', [ '------sanic\r\n' \ From 198bf55b7b47f0344bee3ad5f3246dbaa80847b0 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 14 Jul 2017 17:23:18 -0700 Subject: [PATCH 3/4] flake8 fix --- sanic/request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index eb05f471..27ff011e 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -168,7 +168,9 @@ class Request(dict): 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 + addr for addr in [ + addr.strip() for addr in forwarded_for + ] if addr ] if len(remote_addrs) > 0: self._remote_addr = remote_addrs[0] From 32be1a649694a704c861d3dc4e9b09165cbbd2df Mon Sep 17 00:00:00 2001 From: Mohamed Akram Date: Thu, 20 Jul 2017 03:02:40 +0400 Subject: [PATCH 4/4] Fix FreeBSD syslog path --- sanic/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index b8637bfa..6ffcf7a1 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -12,7 +12,7 @@ _address_dict = { 'Windows': ('localhost', 514), 'Darwin': '/var/run/syslog', 'Linux': '/dev/log', - 'FreeBSD': '/dev/log' + 'FreeBSD': '/var/run/log' } LOGGING = {