Skip to content

Commit 555c35a

Browse files
committed
Support custom encoding for basic auth credentials
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.
1 parent f8e64fb commit 555c35a

File tree

4 files changed

+33
-9
lines changed

4 files changed

+33
-9
lines changed

aiohttp/client.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""HTTP Client for asyncio."""
22

3-
__all__ = ['request', 'HttpClient', 'BasicAuth']
3+
__all__ = ['request', 'HttpClient', 'BasicAuth', 'BasicAuthEx']
44

55
import asyncio
66
import base64
@@ -30,6 +30,8 @@
3030
HTTPS_PORT = 443
3131

3232
BasicAuth = collections.namedtuple('BasicAuth', ['login', 'password'])
33+
BasicAuthEx = collections.namedtuple('BasicAuthEx',
34+
['login', 'password', 'encoding'])
3335

3436

3537
@asyncio.coroutine
@@ -354,17 +356,22 @@ def update_auth(self, auth):
354356
if auth is None:
355357
return
356358

357-
if not isinstance(auth, BasicAuth):
359+
if not isinstance(auth, (BasicAuth, BasicAuthEx)):
358360
warnings.warn(
359-
'BasicAuth() tuple is required instead ', DeprecationWarning)
361+
'BasicAuth() or BasicAuthEx() tuple is required instead ',
362+
DeprecationWarning)
360363

361-
basic_login, basic_passwd = auth
364+
if isinstance(auth, BasicAuthEx):
365+
basic_login, basic_passwd, encoding = auth
366+
else:
367+
basic_login, basic_passwd = auth
368+
encoding = 'latin1'
362369

363370
if basic_login is not None and basic_passwd is not None:
364371
self.headers['AUTHORIZATION'] = 'Basic %s' % (
365372
base64.b64encode(
366-
('%s:%s' % (basic_login, basic_passwd)).encode('latin1'))
367-
.strip().decode('latin1'))
373+
('%s:%s' % (basic_login, basic_passwd)).encode(encoding))
374+
.strip().decode(encoding))
368375
elif basic_login is not None or basic_passwd is not None:
369376
raise ValueError("HTTP Auth login or password is missing")
370377

aiohttp/connector.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .errors import HttpProxyError
1414
from .errors import ProxyConnectionError
15-
from .client import ClientRequest, BasicAuth
15+
from .client import ClientRequest, BasicAuth, BasicAuthEx
1616

1717

1818
class Connection(object):
@@ -285,8 +285,10 @@ def __init__(self, proxy, *args, proxy_auth=None, **kwargs):
285285
self._proxy_auth = proxy_auth
286286
assert proxy.startswith('http://'), (
287287
"Only http proxy supported", proxy)
288-
assert proxy_auth is None or isinstance(proxy_auth, BasicAuth), (
289-
"proxy_auth must be None or BasicAuth() tuple", proxy_auth)
288+
assert (proxy_auth is None
289+
or isinstance(proxy_auth, (BasicAuth, BasicAuthEx))), \
290+
("proxy_auth must be None, BasicAuth() or BasicAuthEx() tuple",
291+
proxy_auth)
290292

291293
@property
292294
def proxy(self):

tests/test_client.py

+7
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ def test_basic_auth(self):
395395
self.assertIn('AUTHORIZATION', req.headers)
396396
self.assertEqual('Basic bmtpbToxMjM0', req.headers['AUTHORIZATION'])
397397

398+
def test_basic_auth_utf8(self):
399+
req = ClientRequest('get', 'http://python.org',
400+
auth=aiohttp.BasicAuthEx('nkim', 'секрет', 'utf-8'))
401+
self.assertIn('AUTHORIZATION', req.headers)
402+
self.assertEqual('Basic bmtpbTrRgdC10LrRgNC10YI=',
403+
req.headers['AUTHORIZATION'])
404+
398405
def test_basic_auth_tuple_deprecated(self):
399406
req = ClientRequest('get', 'http://python.org', auth=('nkim', '1234'))
400407
self.assertIn('AUTHORIZATION', req.headers)

tests/test_connector.py

+8
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,14 @@ def test_auth(self, ClientRequestMock):
426426
auth=aiohttp.BasicAuth('user', 'pass'),
427427
loop=unittest.mock.ANY, headers=unittest.mock.ANY)
428428

429+
@unittest.mock.patch('aiohttp.connector.ClientRequest')
430+
def test_auth_utf8(self, ClientRequestMock):
431+
proxy_req = ClientRequest(
432+
'GET', 'http://proxy.example.com',
433+
auth=aiohttp.BasicAuthEx('юзер', 'пасс', 'utf-8'))
434+
ClientRequestMock.return_value = proxy_req
435+
self.assertIn('AUTHORIZATION', proxy_req.headers)
436+
429437
@unittest.mock.patch('aiohttp.connector.ClientRequest')
430438
def test_auth_from_url(self, ClientRequestMock):
431439
proxy_req = ClientRequest('GET', 'http://user:pass@proxy.example.com')

0 commit comments

Comments
 (0)