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

Skip to content

Commit b0deeb4

Browse files
committed
#25446: Fix regression in smtplib's AUTH LOGIN support.
The auth method tests simply weren't adequate because of the fact that smtpd doesn't support authentication. I borrowed some of Milan's code for that from issue #21935 and added it to the smtplib tests. Also discovered that the direct test for the 'auth' method wasn't actually testing anything and fixed it. The fix makes the new authobject mechanism work the way it is documented...the problem was that wasn't checking for a 334 return code if an initial-response was provided, which works fine for auth plain and cram-md5, but not for auth login.
1 parent 65b77d6 commit b0deeb4

3 files changed

Lines changed: 132 additions & 68 deletions

File tree

Lib/smtplib.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -630,12 +630,12 @@ def auth(self, mechanism, authobject, *, initial_response_ok=True):
630630
(code, resp) = self.docmd("AUTH", mechanism + " " + response)
631631
else:
632632
(code, resp) = self.docmd("AUTH", mechanism)
633-
# Server replies with 334 (challenge) or 535 (not supported)
634-
if code == 334:
635-
challenge = base64.decodebytes(resp)
636-
response = encode_base64(
637-
authobject(challenge).encode('ascii'), eol='')
638-
(code, resp) = self.docmd(response)
633+
# If server responds with a challenge, send the response.
634+
if code == 334:
635+
challenge = base64.decodebytes(resp)
636+
response = encode_base64(
637+
authobject(challenge).encode('ascii'), eol='')
638+
(code, resp) = self.docmd(response)
639639
if code in (235, 503):
640640
return (code, resp)
641641
raise SMTPAuthenticationError(code, resp)
@@ -657,11 +657,10 @@ def auth_plain(self, challenge=None):
657657
def auth_login(self, challenge=None):
658658
""" Authobject to use with LOGIN authentication. Requires self.user and
659659
self.password to be set."""
660-
(code, resp) = self.docmd(
661-
encode_base64(self.user.encode('ascii'), eol=''))
662-
if code == 334:
660+
if challenge is None:
661+
return self.user
662+
else:
663663
return self.password
664-
raise SMTPAuthenticationError(code, resp)
665664

666665
def login(self, user, password, *, initial_response_ok=True):
667666
"""Log in on an SMTP server that requires authentication.

Lib/test/test_smtplib.py

Lines changed: 121 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import asyncore
2+
import base64
23
import email.mime.text
34
from email.message import EmailMessage
45
from email.base64mime import body_encode as encode_base64
56
import email.utils
7+
import hmac
68
import socket
79
import smtpd
810
import smtplib
@@ -623,20 +625,12 @@ def testLineTooLong(self):
623625
sim_auth = ('[email protected]', 'somepassword')
624626
sim_cram_md5_challenge = ('PENCeUxFREJoU0NnbmhNWitOMjNGNn'
625627
'dAZWx3b29kLmlubm9zb2Z0LmNvbT4=')
626-
sim_auth_credentials = {
627-
'login': 'TXIuQUBzb21ld2hlcmUuY29t',
628-
'plain': 'AE1yLkFAc29tZXdoZXJlLmNvbQBzb21lcGFzc3dvcmQ=',
629-
'cram-md5': ('TXIUQUBZB21LD2HLCMUUY29TIDG4OWQ0MJ'
630-
'KWZGQ4ODNMNDA4NTGXMDRLZWMYZJDMODG1'),
631-
}
632-
sim_auth_login_user = 'TXIUQUBZB21LD2HLCMUUY29T'
633-
sim_auth_plain = 'AE1YLKFAC29TZXDOZXJLLMNVBQBZB21LCGFZC3DVCMQ='
634-
635628
sim_lists = {'list-1':['[email protected]','[email protected]'],
636629
'list-2':['[email protected]',],
637630
}
638631

639632
# Simulated SMTP channel & server
633+
class ResponseException(Exception): pass
640634
class SimSMTPChannel(smtpd.SMTPChannel):
641635

642636
quit_response = None
@@ -646,12 +640,109 @@ class SimSMTPChannel(smtpd.SMTPChannel):
646640
rcpt_count = 0
647641
rset_count = 0
648642
disconnect = 0
643+
AUTH = 99 # Add protocol state to enable auth testing.
644+
authenticated_user = None
649645

650646
def __init__(self, extra_features, *args, **kw):
651647
self._extrafeatures = ''.join(
652648
[ "250-{0}\r\n".format(x) for x in extra_features ])
653649
super(SimSMTPChannel, self).__init__(*args, **kw)
654650

651+
# AUTH related stuff. It would be nice if support for this were in smtpd.
652+
def found_terminator(self):
653+
if self.smtp_state == self.AUTH:
654+
line = self._emptystring.join(self.received_lines)
655+
print('Data:', repr(line), file=smtpd.DEBUGSTREAM)
656+
self.received_lines = []
657+
try:
658+
self.auth_object(line)
659+
except ResponseException as e:
660+
self.smtp_state = self.COMMAND
661+
self.push('%s %s' % (e.smtp_code, e.smtp_error))
662+
return
663+
super().found_terminator()
664+
665+
666+
def smtp_AUTH(self, arg):
667+
if not self.seen_greeting:
668+
self.push('503 Error: send EHLO first')
669+
return
670+
if not self.extended_smtp or 'AUTH' not in self._extrafeatures:
671+
self.push('500 Error: command "AUTH" not recognized')
672+
return
673+
if self.authenticated_user is not None:
674+
self.push(
675+
'503 Bad sequence of commands: already authenticated')
676+
return
677+
args = arg.split()
678+
if len(args) not in [1, 2]:
679+
self.push('501 Syntax: AUTH <mechanism> [initial-response]')
680+
return
681+
auth_object_name = '_auth_%s' % args[0].lower().replace('-', '_')
682+
try:
683+
self.auth_object = getattr(self, auth_object_name)
684+
except AttributeError:
685+
self.push('504 Command parameter not implemented: unsupported '
686+
' authentication mechanism {!r}'.format(auth_object_name))
687+
return
688+
self.smtp_state = self.AUTH
689+
self.auth_object(args[1] if len(args) == 2 else None)
690+
691+
def _authenticated(self, user, valid):
692+
if valid:
693+
self.authenticated_user = user
694+
self.push('235 Authentication Succeeded')
695+
else:
696+
self.push('535 Authentication credentials invalid')
697+
self.smtp_state = self.COMMAND
698+
699+
def _decode_base64(self, string):
700+
return base64.decodebytes(string.encode('ascii')).decode('utf-8')
701+
702+
def _auth_plain(self, arg=None):
703+
if arg is None:
704+
self.push('334 ')
705+
else:
706+
logpass = self._decode_base64(arg)
707+
try:
708+
*_, user, password = logpass.split('\0')
709+
except ValueError as e:
710+
self.push('535 Splitting response {!r} into user and password'
711+
' failed: {}'.format(logpass, e))
712+
return
713+
self._authenticated(user, password == sim_auth[1])
714+
715+
def _auth_login(self, arg=None):
716+
if arg is None:
717+
# base64 encoded 'Username:'
718+
self.push('334 VXNlcm5hbWU6')
719+
elif not hasattr(self, '_auth_login_user'):
720+
self._auth_login_user = self._decode_base64(arg)
721+
# base64 encoded 'Password:'
722+
self.push('334 UGFzc3dvcmQ6')
723+
else:
724+
password = self._decode_base64(arg)
725+
self._authenticated(self._auth_login_user, password == sim_auth[1])
726+
del self._auth_login_user
727+
728+
def _auth_cram_md5(self, arg=None):
729+
if arg is None:
730+
self.push('334 {}'.format(sim_cram_md5_challenge))
731+
else:
732+
logpass = self._decode_base64(arg)
733+
try:
734+
user, hashed_pass = logpass.split()
735+
except ValueError as e:
736+
self.push('535 Splitting response {!r} into user and password'
737+
'failed: {}'.format(logpass, e))
738+
return False
739+
valid_hashed_pass = hmac.HMAC(
740+
sim_auth[1].encode('ascii'),
741+
self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
742+
'md5').hexdigest()
743+
self._authenticated(user, hashed_pass == valid_hashed_pass)
744+
# end AUTH related stuff.
745+
655746
def smtp_EHLO(self, arg):
656747
resp = ('250-testhost\r\n'
657748
'250-EXPN\r\n'
@@ -683,20 +774,6 @@ def smtp_EXPN(self, arg):
683774
else:
684775
self.push('550 No access for you!')
685776

686-
def smtp_AUTH(self, arg):
687-
mech = arg.strip().lower()
688-
if mech=='cram-md5':
689-
self.push('334 {}'.format(sim_cram_md5_challenge))
690-
elif mech not in sim_auth_credentials:
691-
self.push('504 auth type unimplemented')
692-
return
693-
elif mech=='plain':
694-
self.push('334 ')
695-
elif mech=='login':
696-
self.push('334 ')
697-
else:
698-
self.push('550 No access for you!')
699-
700777
def smtp_QUIT(self, arg):
701778
if self.quit_response is None:
702779
super(SimSMTPChannel, self).smtp_QUIT(arg)
@@ -841,63 +918,49 @@ def testEXPN(self):
841918
self.assertEqual(smtp.expn(u), expected_unknown)
842919
smtp.quit()
843920

844-
# SimSMTPChannel doesn't fully support AUTH because it requires a
845-
# synchronous read to obtain the credentials...so instead smtpd
846-
# sees the credential sent by smtplib's login method as an unknown command,
847-
# which results in smtplib raising an auth error. Fortunately the error
848-
# message contains the encoded credential, so we can partially check that it
849-
# was generated correctly (partially, because the 'word' is uppercased in
850-
# the error message).
851-
852921
def testAUTH_PLAIN(self):
853922
self.serv.add_feature("AUTH PLAIN")
854923
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
855-
try: smtp.login(sim_auth[0], sim_auth[1], initial_response_ok=False)
856-
except smtplib.SMTPAuthenticationError as err:
857-
self.assertIn(sim_auth_plain, str(err))
924+
resp = smtp.login(sim_auth[0], sim_auth[1])
925+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
858926
smtp.close()
859927

860928
def testAUTH_LOGIN(self):
861929
self.serv.add_feature("AUTH LOGIN")
862930
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
863-
try: smtp.login(sim_auth[0], sim_auth[1])
864-
except smtplib.SMTPAuthenticationError as err:
865-
self.assertIn(sim_auth_login_user, str(err))
931+
resp = smtp.login(sim_auth[0], sim_auth[1])
932+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
866933
smtp.close()
867934

868935
def testAUTH_CRAM_MD5(self):
869936
self.serv.add_feature("AUTH CRAM-MD5")
870937
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
871-
872-
try: smtp.login(sim_auth[0], sim_auth[1])
873-
except smtplib.SMTPAuthenticationError as err:
874-
self.assertIn(sim_auth_credentials['cram-md5'], str(err))
938+
resp = smtp.login(sim_auth[0], sim_auth[1])
939+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
875940
smtp.close()
876941

877942
def testAUTH_multiple(self):
878943
# Test that multiple authentication methods are tried.
879944
self.serv.add_feature("AUTH BOGUS PLAIN LOGIN CRAM-MD5")
880945
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
881-
try: smtp.login(sim_auth[0], sim_auth[1])
882-
except smtplib.SMTPAuthenticationError as err:
883-
self.assertIn(sim_auth_login_user, str(err))
946+
resp = smtp.login(sim_auth[0], sim_auth[1])
947+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
884948
smtp.close()
885949

886950
def test_auth_function(self):
887-
smtp = smtplib.SMTP(HOST, self.port,
888-
local_hostname='localhost', timeout=15)
889-
self.serv.add_feature("AUTH CRAM-MD5")
890-
smtp.user, smtp.password = sim_auth[0], sim_auth[1]
891-
supported = {'CRAM-MD5': smtp.auth_cram_md5,
892-
'PLAIN': smtp.auth_plain,
893-
'LOGIN': smtp.auth_login,
894-
}
895-
for mechanism, method in supported.items():
896-
try: smtp.auth(mechanism, method, initial_response_ok=False)
897-
except smtplib.SMTPAuthenticationError as err:
898-
self.assertIn(sim_auth_credentials[mechanism.lower()].upper(),
899-
str(err))
900-
smtp.close()
951+
supported = {'CRAM-MD5', 'PLAIN', 'LOGIN'}
952+
for mechanism in supported:
953+
self.serv.add_feature("AUTH {}".format(mechanism))
954+
for mechanism in supported:
955+
with self.subTest(mechanism=mechanism):
956+
smtp = smtplib.SMTP(HOST, self.port,
957+
local_hostname='localhost', timeout=15)
958+
smtp.ehlo('foo')
959+
smtp.user, smtp.password = sim_auth[0], sim_auth[1]
960+
method = 'auth_' + mechanism.lower().replace('-', '_')
961+
resp = smtp.auth(mechanism, getattr(smtp, method))
962+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
963+
smtp.close()
901964

902965
def test_quit_resets_greeting(self):
903966
smtp = smtplib.SMTP(HOST, self.port,

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Core and Builtins
5858
Library
5959
-------
6060

61+
- Issue #25446: Fix regression in smtplib's AUTH LOGIN support.
62+
6163
- Issue #18010: Fix the pydoc web server's module search function to handle
6264
exceptions from importing packages.
6365

0 commit comments

Comments
 (0)