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

Skip to content

Commit 6cd6f01

Browse files
committed
Merge: #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.
2 parents 674a42b + 774a39f commit 6cd6f01

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
@@ -185,9 +185,10 @@ An :class:`IMAP4` instance has the following methods:
185185

186186
data = authobject(response)
187187

188-
It will be called to process server continuation responses. It should return
189-
``data`` that will be encoded and sent to server. It should return ``None`` if
190-
the client abort response ``*`` should be sent instead.
188+
It will be called to process server continuation responses; the *response*
189+
argument it is passed will be ``bytes``. It should return ``bytes`` *data*
190+
that will be base64 encoded and sent to the server. It should return
191+
``None`` if the client abort response ``*`` should be sent instead.
191192

192193

193194
.. method:: IMAP4.check()

Lib/imaplib.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,10 @@ def authenticate(self, mechanism, authobject):
352352
353353
data = authobject(response)
354354
355-
It will be called to process server continuation responses.
356-
It should return data that will be encoded and sent to server.
357-
It should return None if the client abort response '*' should
358-
be sent instead.
355+
It will be called to process server continuation responses; the
356+
response argument it is passed will be a bytes. It should return bytes
357+
data that will be base64 encoded and sent to the server. It should
358+
return None if the client abort response '*' should be sent instead.
359359
"""
360360
mech = mechanism.upper()
361361
# XXX: shouldn't this code be removed, not commented out?
@@ -538,7 +538,9 @@ def login_cram_md5(self, user, password):
538538
def _CRAM_MD5_AUTH(self, challenge):
539539
""" Authobject to use with CRAM-MD5 authentication. """
540540
import hmac
541-
return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
541+
pwd = (self.password.encode('ASCII') if isinstance(self.password, str)
542+
else self.password)
543+
return self.user + " " + hmac.HMAC(pwd, challenge).hexdigest()
542544

543545

544546
def logout(self):
@@ -1295,22 +1297,24 @@ def encode(self, inp):
12951297
# so when it gets to the end of the 8-bit input
12961298
# there's no partial 6-bit output.
12971299
#
1298-
oup = ''
1300+
oup = b''
1301+
if isinstance(inp, str):
1302+
inp = inp.encode('ASCII')
12991303
while inp:
13001304
if len(inp) > 48:
13011305
t = inp[:48]
13021306
inp = inp[48:]
13031307
else:
13041308
t = inp
1305-
inp = ''
1309+
inp = b''
13061310
e = binascii.b2a_base64(t)
13071311
if e:
13081312
oup = oup + e[:-1]
13091313
return oup
13101314

13111315
def decode(self, inp):
13121316
if not inp:
1313-
return ''
1317+
return b''
13141318
return binascii.a2b_base64(inp)
13151319

13161320
Months = ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')

Lib/test/test_imaplib.py

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

9696
timeout = 1
97+
continuation = None
98+
capabilities = ''
9799

98100
def _send(self, message):
99101
if verbose: print("SENT: %r" % message.strip())
100102
self.wfile.write(message)
101103

104+
def _send_line(self, message):
105+
self._send(message + b'\r\n')
106+
107+
def _send_textline(self, message):
108+
self._send_line(message.encode('ASCII'))
109+
110+
def _send_tagged(self, tag, code, message):
111+
self._send_textline(' '.join((tag, code, message)))
112+
102113
def handle(self):
103114
# Send a welcome message.
104-
self._send(b'* OK IMAP4rev1\r\n')
115+
self._send_textline('* OK IMAP4rev1')
105116
while 1:
106117
# Gather up input until we receive a line terminator or we timeout.
107118
# Accumulate read(1) because it's simpler to handle the differences
@@ -121,19 +132,33 @@ def handle(self):
121132
break
122133

123134
if verbose: print('GOT: %r' % line.strip())
124-
splitline = line.split()
125-
tag = splitline[0].decode('ASCII')
126-
cmd = splitline[1].decode('ASCII')
135+
if self.continuation:
136+
try:
137+
self.continuation.send(line)
138+
except StopIteration:
139+
self.continuation = None
140+
continue
141+
splitline = line.decode('ASCII').split()
142+
tag = splitline[0]
143+
cmd = splitline[1]
127144
args = splitline[2:]
128145

129146
if hasattr(self, 'cmd_'+cmd):
130-
getattr(self, 'cmd_'+cmd)(tag, args)
147+
continuation = getattr(self, 'cmd_'+cmd)(tag, args)
148+
if continuation:
149+
self.continuation = continuation
150+
next(continuation)
131151
else:
132-
self._send('{} BAD {} unknown\r\n'.format(tag, cmd).encode('ASCII'))
152+
self._send_tagged(tag, 'BAD', cmd + ' unknown')
133153

134154
def cmd_CAPABILITY(self, tag, args):
135-
self._send(b'* CAPABILITY IMAP4rev1\r\n')
136-
self._send('{} OK CAPABILITY completed\r\n'.format(tag).encode('ASCII'))
155+
caps = 'IMAP4rev1 ' + self.capabilities if self.capabilities else 'IMAP4rev1'
156+
self._send_textline('* CAPABILITY ' + caps)
157+
self._send_tagged(tag, 'OK', 'CAPABILITY completed')
158+
159+
def cmd_LOGOUT(self, tag, args):
160+
self._send_textline('* BYE IMAP4ref1 Server logging out')
161+
self._send_tagged(tag, 'OK', 'LOGOUT completed')
137162

138163

139164
class BaseThreadedNetworkedTests(unittest.TestCase):
@@ -183,6 +208,16 @@ def reaped_server(self, hdlr):
183208
finally:
184209
self.reap_server(server, thread)
185210

211+
@contextmanager
212+
def reaped_pair(self, hdlr):
213+
server, thread = self.make_server((support.HOST, 0), hdlr)
214+
client = self.imap_class(*server.server_address)
215+
try:
216+
yield server, client
217+
finally:
218+
client.logout()
219+
self.reap_server(server, thread)
220+
186221
@reap_threads
187222
def test_connect(self):
188223
with self.reaped_server(SimpleIMAPHandler) as server:
@@ -208,12 +243,86 @@ class BadNewlineHandler(SimpleIMAPHandler):
208243

209244
def cmd_CAPABILITY(self, tag, args):
210245
self._send(b'* CAPABILITY IMAP4rev1 AUTH\n')
211-
self._send('{} OK CAPABILITY completed\r\n'.format(tag).encode('ASCII'))
246+
self._send_tagged(tag, 'OK', 'CAPABILITY completed')
212247

213248
with self.reaped_server(BadNewlineHandler) as server:
214249
self.assertRaises(imaplib.IMAP4.abort,
215250
self.imap_class, *server.server_address)
216251

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

218327

219328
class ThreadedNetworkedTests(BaseThreadedNetworkedTests):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ Core and Builtins
181181
Library
182182
-------
183183

184+
- Issue #13700: Fix byte/string handling in imaplib authentication when an
185+
authobject is specified.
186+
184187
- Issue #13153: Tkinter functions now raise TclError instead of ValueError when
185188
a string argument contains non-BMP character.
186189

0 commit comments

Comments
 (0)