Skip to content

Commit

Permalink
Support custom encoding for basic auth credentials
Browse files Browse the repository at this point in the history
This introduces BasicAuthEx namedtuple which acts as like as BasicAuth
one with single exception as it handles encoding as third argument.

RFC2617, section 2 (HTTP Authentication) defines basic-credentials:

    basic-credentials = base64-user-pass
    base64-user-pass  = <base64 encoding of user-pass,
                         except not limited to 76 char/line>
    user-pass         = userid ":" password
    userid            = *<TEXT excluding ":">
    password          = *TEXT

RFC 2616, section 2.1 defines TEXT to have ISO-8859-1 encoding
(aka latin1):

    The TEXT rule is only used for descriptive field contents and values
    that are not intended to be interpreted by the message parser. Words
    of *TEXT MAY contain characters from character sets other than
    ISO-8859-1 only when encoded according to the rules of RFC 2047.

In fact, I know no Basic Auth implementation which respects RFC 2047
for Basic Auth.

However, the truth of the real world is that the most major browsers
are already uses UTF-8 instead of ISO-8859-1 for the credentials as
like as any modern web services which allows non-ASCII login/password.

Also, there is the RFC draft which aims to handle the case when
credentials will optionally get always encoded with UTF-8:
http://tools.ietf.org/html/draft-ietf-httpauth-basicauth-update-01

While we should strictly follow common standards, we also need to handle
real world use cases. This change allows that and makes aiohttp ready
for further Basic Auth scheme standard update.
  • Loading branch information
kxepal committed Jul 8, 2014
1 parent f8e64fb commit 555c35a
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 9 deletions.
19 changes: 13 additions & 6 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""HTTP Client for asyncio."""

__all__ = ['request', 'HttpClient', 'BasicAuth']
__all__ = ['request', 'HttpClient', 'BasicAuth', 'BasicAuthEx']

import asyncio
import base64
Expand Down Expand Up @@ -30,6 +30,8 @@
HTTPS_PORT = 443

BasicAuth = collections.namedtuple('BasicAuth', ['login', 'password'])
BasicAuthEx = collections.namedtuple('BasicAuthEx',
['login', 'password', 'encoding'])


@asyncio.coroutine
Expand Down Expand Up @@ -354,17 +356,22 @@ def update_auth(self, auth):
if auth is None:
return

if not isinstance(auth, BasicAuth):
if not isinstance(auth, (BasicAuth, BasicAuthEx)):
warnings.warn(
'BasicAuth() tuple is required instead ', DeprecationWarning)
'BasicAuth() or BasicAuthEx() tuple is required instead ',
DeprecationWarning)

basic_login, basic_passwd = auth
if isinstance(auth, BasicAuthEx):
basic_login, basic_passwd, encoding = auth
else:
basic_login, basic_passwd = auth
encoding = 'latin1'

if basic_login is not None and basic_passwd is not None:
self.headers['AUTHORIZATION'] = 'Basic %s' % (
base64.b64encode(
('%s:%s' % (basic_login, basic_passwd)).encode('latin1'))
.strip().decode('latin1'))
('%s:%s' % (basic_login, basic_passwd)).encode(encoding))
.strip().decode(encoding))
elif basic_login is not None or basic_passwd is not None:
raise ValueError("HTTP Auth login or password is missing")

Expand Down
8 changes: 5 additions & 3 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .errors import HttpProxyError
from .errors import ProxyConnectionError
from .client import ClientRequest, BasicAuth
from .client import ClientRequest, BasicAuth, BasicAuthEx


class Connection(object):
Expand Down Expand Up @@ -285,8 +285,10 @@ def __init__(self, proxy, *args, proxy_auth=None, **kwargs):
self._proxy_auth = proxy_auth
assert proxy.startswith('http://'), (
"Only http proxy supported", proxy)
assert proxy_auth is None or isinstance(proxy_auth, BasicAuth), (
"proxy_auth must be None or BasicAuth() tuple", proxy_auth)
assert (proxy_auth is None
or isinstance(proxy_auth, (BasicAuth, BasicAuthEx))), \
("proxy_auth must be None, BasicAuth() or BasicAuthEx() tuple",
proxy_auth)

@property
def proxy(self):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ def test_basic_auth(self):
self.assertIn('AUTHORIZATION', req.headers)
self.assertEqual('Basic bmtpbToxMjM0', req.headers['AUTHORIZATION'])

def test_basic_auth_utf8(self):
req = ClientRequest('get', 'http://python.org',
auth=aiohttp.BasicAuthEx('nkim', 'секрет', 'utf-8'))
self.assertIn('AUTHORIZATION', req.headers)
self.assertEqual('Basic bmtpbTrRgdC10LrRgNC10YI=',
req.headers['AUTHORIZATION'])

def test_basic_auth_tuple_deprecated(self):
req = ClientRequest('get', 'http://python.org', auth=('nkim', '1234'))
self.assertIn('AUTHORIZATION', req.headers)
Expand Down
8 changes: 8 additions & 0 deletions tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,14 @@ def test_auth(self, ClientRequestMock):
auth=aiohttp.BasicAuth('user', 'pass'),
loop=unittest.mock.ANY, headers=unittest.mock.ANY)

@unittest.mock.patch('aiohttp.connector.ClientRequest')
def test_auth_utf8(self, ClientRequestMock):
proxy_req = ClientRequest(
'GET', 'http://proxy.example.com',
auth=aiohttp.BasicAuthEx('юзер', 'пасс', 'utf-8'))
ClientRequestMock.return_value = proxy_req
self.assertIn('AUTHORIZATION', proxy_req.headers)

@unittest.mock.patch('aiohttp.connector.ClientRequest')
def test_auth_from_url(self, ClientRequestMock):
proxy_req = ClientRequest('GET', 'http://user:pass@proxy.example.com')
Expand Down

0 comments on commit 555c35a

Please sign in to comment.