From 337346eaecd274d9cbc77aaf44b733396941bcbc Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 19 Jan 2016 08:14:48 -0800 Subject: [PATCH 1/9] Implement BasicAuth decode classmethod. --- aiohttp/helpers.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_helpers.py | 16 ++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 5ddc141d8fb..81dac3bfd8f 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -1,5 +1,6 @@ """Various helper functions""" import base64 +import binascii import datetime import functools import io @@ -31,6 +32,41 @@ def __new__(cls, login, password='', encoding='latin1'): return super().__new__(cls, login, password, encoding) + @classmethod + def decode(cls, auth_header, encoding='latin1'): + """Create a :class:`BasicAuth` object from an ``Authorization`` HTTP + header. + + :param auth_header: The value of the ``Authorization`` header. + :type auth_header: str + :param encoding: The character encoding used on the password. + :type encoding: str + + :returns: The decoded authentication. + :rtype: :class:`BasicAuth` + + :raises ValueError: if the headers are unable to be decoded. + + """ + split = auth_header.strip().split(' ') + if len(split) == 2: + if split[0].strip().lower() != 'basic': + raise ValueError('Unknown authorization method %s' % split[0]) + to_decode = split[1] + elif len(split) == 1: + to_decode = split[0] + else: + raise ValueError('Could not parse authorization header.') + + try: + username, _, password = base64.b64decode( + to_decode.encode('ascii') + ).decode(encoding).partition(':') + except binascii.Error: + raise ValueError('Invalid base64 encoding.') + + return cls(username, password) + def encode(self): """Encode credentials.""" creds = ('%s:%s' % (self.login, self.password)).encode(self.encoding) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 12e2ae65edb..267abade82f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -71,6 +71,22 @@ def test_basic_auth4(): assert auth.encode() == 'Basic bmtpbTpwd2Q=' +def test_basic_auth_decode(): + auth = helpers.BasicAuth.decode('Basic bmtpbTpwd2Q=') + assert auth.login == 'nkim' + assert auth.password == 'pwd' + + +def test_basic_auth_decode_not_basic(): + with pytest.raises(ValueError): + auth = helpers.BasicAuth.decode('Complex bmtpbTpwd2Q=') + + +def test_basic_auth_decode_bad_base64(): + with pytest.raises(ValueError): + auth = helpers.BasicAuth.decode('Basic bmtpbTpwd2Q') + + def test_invalid_formdata_params(): with pytest.raises(TypeError): helpers.FormData('asdasf') From ef0e9f76189cbe5392f50401e4a4fdd49d1c58d4 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 19 Jan 2016 08:30:36 -0800 Subject: [PATCH 2/9] Flake8 fixes. --- tests/test_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 267abade82f..99d15f167c0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -79,12 +79,12 @@ def test_basic_auth_decode(): def test_basic_auth_decode_not_basic(): with pytest.raises(ValueError): - auth = helpers.BasicAuth.decode('Complex bmtpbTpwd2Q=') + helpers.BasicAuth.decode('Complex bmtpbTpwd2Q=') def test_basic_auth_decode_bad_base64(): with pytest.raises(ValueError): - auth = helpers.BasicAuth.decode('Basic bmtpbTpwd2Q') + helpers.BasicAuth.decode('Basic bmtpbTpwd2Q') def test_invalid_formdata_params(): From ef37fa9069ccabf1904b0e80f73280531380e53a Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 19 Jan 2016 08:31:54 -0800 Subject: [PATCH 3/9] Pass through encoding in BasicAuth.decode --- aiohttp/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 81dac3bfd8f..ff14bda1143 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -65,7 +65,7 @@ def decode(cls, auth_header, encoding='latin1'): except binascii.Error: raise ValueError('Invalid base64 encoding.') - return cls(username, password) + return cls(username, password, encoding=encoding) def encode(self): """Encode credentials.""" From 9e8bd58e2a488fc1306e9621e3ec8ce9e44f0e5c Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 19 Jan 2016 10:33:40 -0800 Subject: [PATCH 4/9] Don't be so forgiving when parsing basic auth. --- aiohttp/helpers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index ff14bda1143..bf98169a1d4 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -53,8 +53,6 @@ def decode(cls, auth_header, encoding='latin1'): if split[0].strip().lower() != 'basic': raise ValueError('Unknown authorization method %s' % split[0]) to_decode = split[1] - elif len(split) == 1: - to_decode = split[0] else: raise ValueError('Could not parse authorization header.') From 3eff09a36d7b11481490e21a75f8067d00c3962a Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 19 Jan 2016 10:44:48 -0800 Subject: [PATCH 5/9] Implement `Request.authorization` --- aiohttp/web_reqrep.py | 18 +++++++++++++++++- tests/test_web_request.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index b001a4223bb..837f2040a57 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -17,7 +17,7 @@ from urllib.parse import urlsplit, parse_qsl, unquote from . import hdrs -from .helpers import reify +from .helpers import reify, BasicAuth from .multidict import (CIMultiDictProxy, CIMultiDict, MultiDictProxy, @@ -239,6 +239,22 @@ def if_modified_since(self, _IF_MODIFIED_SINCE=hdrs.IF_MODIFIED_SINCE): tzinfo=datetime.timezone.utc) return None + @reify + def authorization(self, _AUTHORIZATION=hdrs.AUTHORIZATION): + """Parse the Authorization header and return the username and password, + or ``None`` if the header is absent or cannot be parsed. + + :rtype: :class:`aiohttp.helpers.BasicAuth` + + """ + header = self.headers.get(_AUTHORIZATION) + if header is not None: + try: + return BasicAuth.decode(header) + except ValueError: + return None + return None + @reify def keep_alive(self): """Is keepalive enabled by client?""" diff --git a/tests/test_web_request.py b/tests/test_web_request.py index b757df9f3c0..da16539bae0 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -5,6 +5,7 @@ from aiohttp.multidict import MultiDict, CIMultiDict from aiohttp.protocol import HttpVersion from aiohttp.protocol import RawRequestMessage +from aiohttp.helpers import BasicAuth @pytest.fixture @@ -192,6 +193,23 @@ def test_request_cookie__set_item(make_request): req.cookies['my'] = 'value' +def test_request_authorization(make_request): + headers = CIMultiDict(AUTHORIZATION='Basic bmtpbTpwd2Q=') + req = make_request('GET', '/', headers=headers) + assert req.authorization == BasicAuth(login='nkim', password='pwd') + + +def test_request_authorization_absent(make_request): + req = make_request('GET', '/') + assert req.authorization is None + + +def test_request_authorization_invalid(make_request): + headers = CIMultiDict(AUTHORIZATION='Foobar') + req = make_request('GET', '/', headers=headers) + assert req.authorization is None + + def test_match_info(make_request): req = make_request('GET', '/') assert req.match_info is None From 06b5f0e9d3d7a357676d17a010c2efcbecfca014 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Tue, 3 May 2016 09:06:55 -0700 Subject: [PATCH 6/9] Revert "Implement `Request.authorization`" This reverts commit 3eff09a36d7b11481490e21a75f8067d00c3962a. --- aiohttp/web_reqrep.py | 18 +----------------- tests/test_web_request.py | 18 ------------------ 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index 837f2040a57..b001a4223bb 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -17,7 +17,7 @@ from urllib.parse import urlsplit, parse_qsl, unquote from . import hdrs -from .helpers import reify, BasicAuth +from .helpers import reify from .multidict import (CIMultiDictProxy, CIMultiDict, MultiDictProxy, @@ -239,22 +239,6 @@ def if_modified_since(self, _IF_MODIFIED_SINCE=hdrs.IF_MODIFIED_SINCE): tzinfo=datetime.timezone.utc) return None - @reify - def authorization(self, _AUTHORIZATION=hdrs.AUTHORIZATION): - """Parse the Authorization header and return the username and password, - or ``None`` if the header is absent or cannot be parsed. - - :rtype: :class:`aiohttp.helpers.BasicAuth` - - """ - header = self.headers.get(_AUTHORIZATION) - if header is not None: - try: - return BasicAuth.decode(header) - except ValueError: - return None - return None - @reify def keep_alive(self): """Is keepalive enabled by client?""" diff --git a/tests/test_web_request.py b/tests/test_web_request.py index da16539bae0..b757df9f3c0 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -5,7 +5,6 @@ from aiohttp.multidict import MultiDict, CIMultiDict from aiohttp.protocol import HttpVersion from aiohttp.protocol import RawRequestMessage -from aiohttp.helpers import BasicAuth @pytest.fixture @@ -193,23 +192,6 @@ def test_request_cookie__set_item(make_request): req.cookies['my'] = 'value' -def test_request_authorization(make_request): - headers = CIMultiDict(AUTHORIZATION='Basic bmtpbTpwd2Q=') - req = make_request('GET', '/', headers=headers) - assert req.authorization == BasicAuth(login='nkim', password='pwd') - - -def test_request_authorization_absent(make_request): - req = make_request('GET', '/') - assert req.authorization is None - - -def test_request_authorization_invalid(make_request): - headers = CIMultiDict(AUTHORIZATION='Foobar') - req = make_request('GET', '/', headers=headers) - assert req.authorization is None - - def test_match_info(make_request): req = make_request('GET', '/') assert req.match_info is None From c351ca71ed8edc6fc52e66138f44b5a08a682681 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 9 May 2016 13:13:05 -0700 Subject: [PATCH 7/9] Document. --- docs/client_reference.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 8da3a94d996..69cea594ccf 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1290,6 +1290,13 @@ BasicAuth e.g. *auth* parameter for :meth:`ClientSession.request`. + .. classmethod:: decode() + + Decode HTTP basic authentication credentials. + + :return: decoded authentication data, :class:`BasicAuth`. + + .. method:: encode() Encode credentials into string suitable for ``Authorization`` From 1f14c405134dc919ff96adf4d4185d354e015a03 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 23 May 2016 12:07:41 -0700 Subject: [PATCH 8/9] Remove docstring. --- aiohttp/helpers.py | 14 +------------- docs/client_reference.rst | 5 ++++- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 861820599c3..5a74259eb3a 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -46,19 +46,7 @@ def __new__(cls, login, password='', encoding='latin1'): @classmethod def decode(cls, auth_header, encoding='latin1'): """Create a :class:`BasicAuth` object from an ``Authorization`` HTTP - header. - - :param auth_header: The value of the ``Authorization`` header. - :type auth_header: str - :param encoding: The character encoding used on the password. - :type encoding: str - - :returns: The decoded authentication. - :rtype: :class:`BasicAuth` - - :raises ValueError: if the headers are unable to be decoded. - - """ + header.""" split = auth_header.strip().split(' ') if len(split) == 2: if split[0].strip().lower() != 'basic': diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 69cea594ccf..c446a76d56d 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1290,10 +1290,13 @@ BasicAuth e.g. *auth* parameter for :meth:`ClientSession.request`. - .. classmethod:: decode() + .. classmethod:: decode(auth_header, encoding='latin1') Decode HTTP basic authentication credentials. + :param str auth_header: The ``Authorization`` header to decode. + :param str encoding: (optional) encoding ('latin1' by default) + :return: decoded authentication data, :class:`BasicAuth`. From 46db2687d7c843e477f9508fa2fe95a91f6cc7a6 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 23 May 2016 12:26:34 -0700 Subject: [PATCH 9/9] Add additional test to increase coverage. --- tests/test_helpers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f2b07fdcc27..4287cf0a3cd 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -77,6 +77,11 @@ def test_basic_auth_decode(): assert auth.password == 'pwd' +def test_basic_auth_invalid(): + with pytest.raises(ValueError): + helpers.BasicAuth.decode('bmtpbTpwd2Q=') + + def test_basic_auth_decode_not_basic(): with pytest.raises(ValueError): helpers.BasicAuth.decode('Complex bmtpbTpwd2Q=')