Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 774a39f

Browse files
committed
#13700: Make imap.authenticate with authobject work.
This fixes a bytes/string confusion in the API which prevented custom authobjects from working at all. Original patch by Erno Tukia.
1 parent 6b30759 commit 774a39f

4 files changed

Lines changed: 137 additions & 20 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,10 @@ An :class:`IMAP4` instance has the following methods:
176176

177177
data = authobject(response)
178178

179-
It will be called to process server continuation responses. It should return
180-
``data`` that will be encoded and sent to server. It should return ``None`` if
181-
the client abort response ``*`` should be sent instead.
179+
It will be called to process server continuation responses; the *response*
180+
argument it is passed will be ``bytes``. It should return ``bytes`` *data*
181+
that will be base64 encoded and sent to the server. It should return
182+
``None`` if the client abort response ``*`` should be sent instead.
182183

183184

184185
.. method:: IMAP4.check()

Lib/imaplib.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,10 +360,10 @@ def authenticate(self, mechanism, authobject):
360360
361361
data = authobject(response)
362362
363-
It will be called to process server continuation responses.
364-
It should return data that will be encoded and sent to server.
365-
It should return None if the client abort response '*' should
366-
be sent instead.
363+
It will be called to process server continuation responses; the
364+
response argument it is passed will be a bytes. It should return bytes
365+
data that will be base64 encoded and sent to the server. It should
366+
return None if the client abort response '*' should be sent instead.
367367
"""
368368
mech = mechanism.upper()
369369
# XXX: shouldn't this code be removed, not commented out?
@@ -546,7 +546,9 @@ def login_cram_md5(self, user, password):
546546
def _CRAM_MD5_AUTH(self, challenge):
547547
""" Authobject to use with CRAM-MD5 authentication. """
548548
import hmac
549-
return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
549+
pwd = (self.password.encode('ASCII') if isinstance(self.password, str)
550+
else self.password)
551+
return self.user + " " + hmac.HMAC(pwd, challenge).hexdigest()
550552

551553

552554
def logout(self):
@@ -1288,22 +1290,24 @@ def encode(self, inp):
12881290
# so when it gets to the end of the 8-bit input
12891291
# there's no partial 6-bit output.
12901292
#
1291-
oup = ''
1293+
oup = b''
1294+
if isinstance(inp, str):
1295+
inp = inp.encode('ASCII')
12921296
while inp:
12931297
if len(inp) > 48:
12941298
t = inp[:48]
12951299
inp = inp[48:]
12961300
else:
12971301
t = inp
1298-
inp = ''
1302+
inp = b''
12991303
e = binascii.b2a_base64(t)
13001304
if e:
13011305
oup = oup + e[:-1]
13021306
return oup
13031307

13041308
def decode(self, inp):
13051309
if not inp:
1306-
return ''
1310+
return b''
13071311
return binascii.a2b_base64(inp)
13081312

13091313

Lib/test/test_imaplib.py

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,25 @@ class SecureTCPServer:
7878
class SimpleIMAPHandler(socketserver.StreamRequestHandler):
7979

8080
timeout = 1
81+
continuation = None
82+
capabilities = ''
8183

8284
def _send(self, message):
8385
if verbose: print("SENT: %r" % message.strip())
8486
self.wfile.write(message)
8587

88+
def _send_line(self, message):
89+
self._send(message + b'\r\n')
90+
91+
def _send_textline(self, message):
92+
self._send_line(message.encode('ASCII'))
93+
94+
def _send_tagged(self, tag, code, message):
95+
self._send_textline(' '.join((tag, code, message)))
96+
8697
def handle(self):
8798
# Send a welcome message.
88-
self._send(b'* OK IMAP4rev1\r\n')
99+
self._send_textline('* OK IMAP4rev1')
89100
while 1:
90101
# Gather up input until we receive a line terminator or we timeout.
91102
# Accumulate read(1) because it's simpler to handle the differences
@@ -105,19 +116,33 @@ def handle(self):
105116
break
106117

107118
if verbose: print('GOT: %r' % line.strip())
108-
splitline = line.split()
109-
tag = splitline[0].decode('ASCII')
110-
cmd = splitline[1].decode('ASCII')
119+
if self.continuation:
120+
try:
121+
self.continuation.send(line)
122+
except StopIteration:
123+
self.continuation = None
124+
continue
125+
splitline = line.decode('ASCII').split()
126+
tag = splitline[0]
127+
cmd = splitline[1]
111128
args = splitline[2:]
112129

113130
if hasattr(self, 'cmd_'+cmd):
114-
getattr(self, 'cmd_'+cmd)(tag, args)
131+
continuation = getattr(self, 'cmd_'+cmd)(tag, args)
132+
if continuation:
133+
self.continuation = continuation
134+
next(continuation)
115135
else:
116-
self._send('{} BAD {} unknown\r\n'.format(tag, cmd).encode('ASCII'))
136+
self._send_tagged(tag, 'BAD', cmd + ' unknown')
117137

118138
def cmd_CAPABILITY(self, tag, args):
119-
self._send(b'* CAPABILITY IMAP4rev1\r\n')
120-
self._send('{} OK CAPABILITY completed\r\n'.format(tag).encode('ASCII'))
139+
caps = 'IMAP4rev1 ' + self.capabilities if self.capabilities else 'IMAP4rev1'
140+
self._send_textline('* CAPABILITY ' + caps)
141+
self._send_tagged(tag, 'OK', 'CAPABILITY completed')
142+
143+
def cmd_LOGOUT(self, tag, args):
144+
self._send_textline('* BYE IMAP4ref1 Server logging out')
145+
self._send_tagged(tag, 'OK', 'LOGOUT completed')
121146

122147

123148
class BaseThreadedNetworkedTests(unittest.TestCase):
@@ -167,6 +192,16 @@ def reaped_server(self, hdlr):
167192
finally:
168193
self.reap_server(server, thread)
169194

195+
@contextmanager
196+
def reaped_pair(self, hdlr):
197+
server, thread = self.make_server((support.HOST, 0), hdlr)
198+
client = self.imap_class(*server.server_address)
199+
try:
200+
yield server, client
201+
finally:
202+
client.logout()
203+
self.reap_server(server, thread)
204+
170205
@reap_threads
171206
def test_connect(self):
172207
with self.reaped_server(SimpleIMAPHandler) as server:
@@ -192,12 +227,86 @@ class BadNewlineHandler(SimpleIMAPHandler):
192227

193228
def cmd_CAPABILITY(self, tag, args):
194229
self._send(b'* CAPABILITY IMAP4rev1 AUTH\n')
195-
self._send('{} OK CAPABILITY completed\r\n'.format(tag).encode('ASCII'))
230+
self._send_tagged(tag, 'OK', 'CAPABILITY completed')
196231

197232
with self.reaped_server(BadNewlineHandler) as server:
198233
self.assertRaises(imaplib.IMAP4.abort,
199234
self.imap_class, *server.server_address)
200235

236+
@reap_threads
237+
def test_bad_auth_name(self):
238+
239+
class MyServer(SimpleIMAPHandler):
240+
241+
def cmd_AUTHENTICATE(self, tag, args):
242+
self._send_tagged(tag, 'NO', 'unrecognized authentication '
243+
'type {}'.format(args[0]))
244+
245+
with self.reaped_pair(MyServer) as (server, client):
246+
with self.assertRaises(imaplib.IMAP4.error):
247+
client.authenticate('METHOD', lambda: 1)
248+
249+
@reap_threads
250+
def test_invalid_authentication(self):
251+
252+
class MyServer(SimpleIMAPHandler):
253+
254+
def cmd_AUTHENTICATE(self, tag, args):
255+
self._send_textline('+')
256+
self.response = yield
257+
self._send_tagged(tag, 'NO', '[AUTHENTICATIONFAILED] invalid')
258+
259+
with self.reaped_pair(MyServer) as (server, client):
260+
with self.assertRaises(imaplib.IMAP4.error):
261+
code, data = client.authenticate('MYAUTH', lambda x: b'fake')
262+
263+
@reap_threads
264+
def test_valid_authentication(self):
265+
266+
class MyServer(SimpleIMAPHandler):
267+
268+
def cmd_AUTHENTICATE(self, tag, args):
269+
self._send_textline('+')
270+
self.server.response = yield
271+
self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
272+
273+
with self.reaped_pair(MyServer) as (server, client):
274+
code, data = client.authenticate('MYAUTH', lambda x: b'fake')
275+
self.assertEqual(code, 'OK')
276+
self.assertEqual(server.response,
277+
b'ZmFrZQ==\r\n') #b64 encoded 'fake'
278+
279+
with self.reaped_pair(MyServer) as (server, client):
280+
code, data = client.authenticate('MYAUTH', lambda x: 'fake')
281+
self.assertEqual(code, 'OK')
282+
self.assertEqual(server.response,
283+
b'ZmFrZQ==\r\n') #b64 encoded 'fake'
284+
285+
@reap_threads
286+
def test_login_cram_md5(self):
287+
288+
class AuthHandler(SimpleIMAPHandler):
289+
290+
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
291+
292+
def cmd_AUTHENTICATE(self, tag, args):
293+
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
294+
'VzdG9uLm1jaS5uZXQ=')
295+
r = yield
296+
if r == b'dGltIGYxY2E2YmU0NjRiOWVmYTFjY2E2ZmZkNmNmMmQ5ZjMy\r\n':
297+
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
298+
else:
299+
self._send_tagged(tag, 'NO', 'No access')
300+
301+
with self.reaped_pair(AuthHandler) as (server, client):
302+
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
303+
ret, data = client.login_cram_md5("tim", "tanstaaftanstaaf")
304+
self.assertEqual(ret, "OK")
305+
306+
with self.reaped_pair(AuthHandler) as (server, client):
307+
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
308+
ret, data = client.login_cram_md5("tim", b"tanstaaftanstaaf")
309+
self.assertEqual(ret, "OK")
201310

202311

203312
class ThreadedNetworkedTests(BaseThreadedNetworkedTests):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ Core and Builtins
227227
Library
228228
-------
229229

230+
- Issue #13700: Fix byte/string handling in imaplib authentication when an
231+
authobject is specified.
232+
230233
- Issue #13153: Tkinter functions now raise TclError instead of ValueError when
231234
a string argument contains non-BMP character.
232235

0 commit comments

Comments
 (0)