[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[pysieved] SASL revamp
- From: Philippe Levan <levan at epix dot net>
- Subject: [pysieved] SASL revamp
- Date: Sun, 29 Jul 2007 12:06:34 -0400 (EDT)
OK, so I did get ambitious (actually, I couldn't
get any sleep :P) and I decided to take a stab
at revamping the SASL code as a pass-through
to Dovecot.
Here's the patch for the monstruosity I came up
with. It was successfully tested with CRAM-MD5
as well as PLAIN (I can't figure our DIGEST-MD5
but I think it's only a configuration issue).
The changes include :
. minor cosmetic logging changes
. the AUTHENTICATE code supports continuation
via do_sasl_first() and do_sasl_next()
. do_sasl_first() is defined by default to be
a wrapper around the auth() method
. do_sasl_next() should be implemented by
plugins that want to support more complex
SASL exchanges
. acceptable mechanisms can be overridden by
modifying the plugin's mechanisms() method
. reorganization of the dovecot code to take
advantage of the above framework
Thanks to the Postfix-SASL programmers whose
code I used for inspiration.
PS : line 190 of managesieve.py before patching
(if self.debug:) causes an error ('debug'
is not defined) if you hit that piece of
code but I didn't take the time to figure
that one out.
--
Philippe Levan - Frontier/epix Systems
diff -cr pysieved/managesieve.py pysieved.new/managesieve.py
*** pysieved/managesieve.py Fri Jul 27 16:08:09 2007
--- pysieved.new/managesieve.py Sun Jul 29 11:04:43 2007
***************
*** 90,95 ****
--- 90,96 ----
elif len(a) > 200 or '"' in a:
out.append('{%d+}' % len(a))
flush(out)
+ self.log(3, 'S: %r' % a)
self.write(a)
else:
out.append('"%s"' % a)
***************
*** 140,146 ****
if pos > -1:
self.buf = out[pos+2:]
s = out[:pos]
! self.log(3, 'C: %r' % s)
return s
r = self.read(1024)
if not r:
--- 141,147 ----
if pos > -1:
self.buf = out[pos+2:]
s = out[:pos]
! self.log(3, 'C: %r' % (s + '\r\n'))
return s
r = self.read(1024)
if not r:
***************
*** 252,267 ****
if not self.tls:
return self.no(code='ENCRYPT-NEEDED')
! if mechanism.lower() != 'plain':
! return self.no(reason='Unsupported authentication mechanism')
! if len(args) != 1:
! return self.no(reason='Must provide authentication credentials')
!
! _, user, passwd = args[0].decode('base64').split('\0', 2)
! if not self.authenticate(user, passwd):
! return self.no(reason='Bad username or password')
! home = self.get_homedir(user)
self.storage = self.new_storage(home)
return self.ok()
--- 253,294 ----
if not self.tls:
return self.no(code='ENCRYPT-NEEDED')
!
! ret = self.do_sasl_first(mechanism, *args)
!
! while ret['result'] == 'CONT':
! line = ('{%d+}\r\n' % len(ret['msg']))
! self.log(3, 'S: %r' % line)
! self.write(line)
! line = ('%s\r\n' % ret['msg'])
! self.log(3, 'S: %r' % line)
! self.write(line)
! s = self.readline()
! if len(s) > 3 and s[0] == '{' and s[-2:] == '+}':
! try:
! n = int(o[1:-2])
! s = self.bread(n)
! except ValueError:
! pass
! elif len(s) > 1 and s[0] == '"' and s[-1] == '"':
! s = s[1:-1]
!
! ret = self.do_sasl_next(s)
!
! if ret['result'] == 'NO':
! return self.no(reason=ret['msg'])
! elif ret['result'] == 'BYE':
! self.bye(reason=ret['msg'])
! raise Hangup()
!
! home = self.get_homedir(ret['username'])
!
! if not home:
! self.bye(reason='Server Error')
! raise Hangup()
!
self.storage = self.new_storage(home)
+
return self.ok()
***************
*** 293,299 ****
self.send('IMPLEMENTATION', version)
if self.tls:
! self.send('SASL', 'PLAIN')
else:
self.send('SASL')
self.send('SIEVE', self.capabilities)
--- 320,326 ----
self.send('IMPLEMENTATION', version)
if self.tls:
! self.send('SASL', ' '.join(self.list_mech()))
else:
self.send('SASL')
self.send('SIEVE', self.capabilities)
***************
*** 363,370 ****
s = self.storage[name]
except KeyError:
return self.no(reason='No script by that name')
! self.write('{%d+}\r\n' % len(s))
! self.write(s + '\r\n')
return self.ok()
--- 390,401 ----
s = self.storage[name]
except KeyError:
return self.no(reason='No script by that name')
! line = ('{%d+}\r\n' % len(s))
! self.log(3, 'S: %r' % line)
! self.write(line)
! line = ('%s\r\n' % s)
! self.log(3, 'S: %r' % line)
! self.write(line)
return self.ok()
diff -cr pysieved/plugins/__init__.py pysieved.new/plugins/__init__.py
*** pysieved/plugins/__init__.py Mon Jul 23 18:24:17 2007
--- pysieved.new/plugins/__init__.py Sun Jul 29 07:37:13 2007
***************
*** 21,26 ****
--- 21,27 ----
class new:
# Override this
capabilities = 'fileinto reject'
+ mechs = [ 'PLAIN' ]
def __init__(self, log_func, config):
self.log = log_func
***************
*** 30,35 ****
--- 31,77 ----
# Override this
pass
+ def mechanisms(self):
+ return self.mechs
+
+ def do_sasl_first(self, mechanism, *args):
+ """Handle the initial part of the SASL dialog
+
+ This is just a wrapper around the auth() method
+ if the requested mechanism is PLAIN.
+
+ Returns a dictionary with the following elements :
+ { 'result': 'OK' (pass), 'NO' (fail), 'BYE' (fatal) or 'CONT' (more),
+ 'msg': error string (fail or fatal) or server response (more),
+ 'username': authorized username (pass) }
+
+ """
+
+ if mechanism.upper() not in [ mech.upper() for mech in self.mechs ]:
+ return {'result': 'NO', 'msg': 'Unsupported authentication mechanism'}
+ if len(args) != 1:
+ return {'result': 'NO', 'msg': 'Must provide authentication credentials'}
+
+ _, user, passwd = args[0].decode('base64').split('\0', 2)
+ params = {'username': user, 'password': passwd}
+ if self.auth(params):
+ return {'result': 'OK', 'username': user}
+ else:
+ return {'result': 'NO', 'msg': 'Bad username or password'}
+
+
+ def do_sasl_next(self, b64_string):
+ """Handle the continuation of the SASL dialog
+
+ Returns a dictionary with the following elements :
+ { 'result': 'OK' (pass), 'NO' (fail), 'BYE' (fatal) or 'CONT' (more),
+ 'msg': error string (fail or fatal) or server response (more),
+ 'username': authorized username (pass) }
+
+ """
+
+ raise NotImplementedError()
+
def auth(self, params):
"""Authenticate a user.
diff -cr pysieved/plugins/dovecot.py pysieved.new/plugins/dovecot.py
*** pysieved/plugins/dovecot.py Mon Jul 23 18:24:17 2007
--- pysieved.new/plugins/dovecot.py Sun Jul 29 11:13:28 2007
***************
*** 174,179 ****
--- 174,181 ----
capabilities = ('fileinto reject envelope vacation imapflags '
'notify subaddress relational '
'comparator-i;ascii-numeric')
+ mechs = []
+ version = [ 1, 0 ]
def init(self, config):
self.mux = config.get('Dovecot', 'mux', False)
***************
*** 191,251 ****
if self.uid >= 0:
os.setuid(self.uid)
- def auth(self, params):
# We can do this only if a MUX socket was specified
! if self.mux:
! self.auth_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
! self.auth_sock.connect(self.mux)
! handshake_string = self.auth_sock.recv(1024)
! self.auth_sock.sendall('VERSION\t1\t0\nCPID\t%d\n' % os.getpid())
! else:
raise ValueError('No MUX socket was specified')
! # Since this socket will be used only once, use a hardcoded request ID
! auth_string = ('AUTH\t%d\tPLAIN\tservice=%s\tresp=%s' %
! (1,
! self.service,
! base64.encodestring(params['username'] + '\0' +
! params['username'] + '\0' +
! params['password'])))
! self.auth_sock.sendall(auth_string + '\n')
! ret = self.auth_sock.recv(1024)
- self.auth_sock.close();
if ret.startswith('OK'):
return True
return False
def lookup(self, params):
# We can do this only if a master socket was specified
! if self.master:
! self.user_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
! self.user_sock.connect(self.master)
! master_handshake_string = self.user_sock.recv(1024)
! self.user_sock.sendall('VERSION\t1\t0\n')
! else:
raise ValueError('No master socket was specified')
! # Since this socket will be used only once, use a hardcoded request ID
! lookup_string = ('USER\t%d\t%s\tservice=%s' %
! (1,
params['username'],
self.service))
! self.user_sock.sendall(lookup_string + '\n')
ret = self.user_sock.recv(1024)
!
! self.user_sock.close();
if ret.startswith('USER\t'):
! uid = ret[ret.find('\tuid=')+5:]
! uid = uid[:uid.find('\t')]
! gid = ret[ret.find('\tgid=')+5:]
! gid = gid[:gid.find('\t')]
! home = ret[ret.find('\thome=')+6:]
! home = home[:home.find('\t')]
# Assuming we were started with elevated privileges, drop them now
if (self.gid < 0) and (int(gid) >= 0):
--- 193,380 ----
if self.uid >= 0:
os.setuid(self.uid)
+ # No sockets are open
+ self.auth_sock = None
+ self.user_sock = None
+
+
+ def open_auth_socket(self):
+ # The forked child should be short-lived enough that
+ # it should be ok to open the authentication socket
+ # only once
# We can do this only if a MUX socket was specified
! if not self.mux:
raise ValueError('No MUX socket was specified')
! # Open the socket
! self.log(7, 'Opening socket %s' % self.mux)
! self.auth_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
! self.auth_sock.connect(self.mux)
!
! # Send our version and PID
! init_string = ('VERSION\t%d\t%d\nCPID\t%d\n' %
! (self.version[0],
! self.version[1],
! os.getpid()))
! self.log(7, '> %r' % init_string)
! self.auth_sock.sendall(init_string)
!
! # Verify version
! greet = self.auth_sock.recv(1024)
! self.log(7, '< %r' % greet)
! if greet.find('VERSION\t%d\t' % self.version[0]) == -1:
! raise ValueError('Incompatible version number')
!
! # Grab mechanisms
! if len(self.mechs) == 0:
! for line in greet.splitlines():
! if line.startswith('MECH\t'):
! parts = line.split('\t')
! if parts[1].upper() not in [ mech.upper() for mech in self.mechs]:
! self.log(7, 'Adding mechanism %s' % parts[1].upper())
! self.mechs.append(parts[1].upper())
!
! # All done
! self.reqid = 0
!
!
! def mechanisms(self):
! # When first started, no mechanisms are known
! # Open the authentication and get a list from the daemon
! if len(self.mechs) == 0 and not self.auth_sock:
! self.open_auth_socket()
!
! return self.mechs
!
!
! def do_sasl_first(self, mechanism, *args):
! # Make sure the requested mechanism is supported
! if mechanism.upper() not in [ mech.upper() for mech in self.mechs ]:
! return {'result': 'NO',
! 'msg': 'Unsupported authentication mechanism'}
!
! # Build authentication request
! self.reqid = self.reqid + 1
! if len(args) > 0:
! auth_string = ('AUTH\t%d\t%s\tservice=%s\tresp=%s\n' %
! (self.reqid,
! mechanism.upper(),
! self.service,
! args[0]))
! else:
! auth_string = ('AUTH\t%d\t%s\tservice=%s\n' %
! (self.reqid,
! mechanism.upper(),
! self.service))
!
! # We should already have a socket open, just perform the dialog
! return self.do_sasl_dialog(auth_string)
+ def do_sasl_next(self, b64_string):
+ # Build continuation request
+ cont_string = ('CONT\t%d\t%s\n' %
+ (self.reqid,
+ b64_string))
+
+ # We should already have a socket open, just perform the dialog
+ return self.do_sasl_dialog(cont_string)
+
+
+ def do_sasl_dialog(self, msg):
+ # We should have an open socket by now
+ if not self.auth_sock:
+ return {'result': 'BYE', 'msg': 'Server Error'}
+
+ # Dialog
+ self.log(7, '> %r' % msg)
+ self.auth_sock.sendall(msg)
+ ret = self.auth_sock.recv(1024)
+ self.log(7, '< %r' % ret)
+
+ # Parse result
if ret.startswith('OK'):
+ pass
+ elif ret.startswith('FAIL'):
+ return {'result': 'NO', 'msg': 'Authentication failed'}
+ elif ret.startswith('CONT\t'):
+ ret = (ret.splitlines())[0]
+ parts = ret.split('\t')
+ if len(parts) >= 3:
+ return {'result': 'CONT', 'msg': parts[2]}
+ else:
+ return {'result': 'CONT', 'msg': ''}
+ else:
+ return {'result': 'BYE', 'msg': 'Unexpected result'}
+
+ # Extract the authorized user
+ username = None
+
+ ret = (ret.splitlines())[0]
+ for part in ret.split('\t'):
+ if part.startswith('user='):
+ username = part[5:]
+
+ return {'result': 'OK', 'username': username}
+
+
+ def auth(self, params):
+ # Refer to do_sasl_first
+ ret = self.do_sasl_first('PLAIN',
+ base64.encodestring('\0' +
+ params['username'] + '\0' +
+ params['password']))
+
+ if ret['result'].startswith('OK'):
return True
return False
def lookup(self, params):
# We can do this only if a master socket was specified
! if not self.master:
raise ValueError('No master socket was specified')
! if not self.user_sock:
! self.log(7, 'Opening socket %s' % self.master)
! self.user_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
! self.user_sock.connect(self.master)
! init_string = ('VERSION\t%d\t%d\n' %
! (self.version[0],
! self.version[1]))
! self.log(7, '> %r' % init_string)
! self.user_sock.sendall(init_string)
! greet = self.user_sock.recv(1024)
! self.log(7, '< %r' % greet)
! if greet.find('VERSION\t%d\t' % self.version[0]) == -1:
! raise ValueError('Incompatible major version number')
! self.lookup_id = 0
!
! self.lookup_id = self.lookup_id + 1
! lookup_string = ('USER\t%d\t%s\tservice=%s\n' %
! (self.lookup_id,
params['username'],
self.service))
! self.log(7, '> %r' % lookup_string)
! self.user_sock.sendall(lookup_string)
ret = self.user_sock.recv(1024)
! self.log(7, '< %r' % ret)
if ret.startswith('USER\t'):
! ret = (ret.splitlines())[0]
!
! uid = None
! gid = None
! home = None
!
! for part in ret.split('\t'):
! if part.startswith('uid='):
! uid = part[4:]
! elif part.startswith('gid='):
! gid = part[4:]
! elif part.startswith('home='):
! home = part[5:]
# Assuming we were started with elevated privileges, drop them now
if (self.gid < 0) and (int(gid) >= 0):
diff -cr pysieved/pysieved.py pysieved.new/pysieved.py
*** pysieved/pysieved.py Fri Jul 27 15:01:15 2007
--- pysieved.new/pysieved.py Sun Jul 29 06:44:14 2007
***************
*** 115,120 ****
--- 115,143 ----
def log(self, l, s):
log(l, s)
+ def list_mech(self):
+ mechs = authenticate.mechanisms()
+ self.log(5, "Announcing mechanisms : %r" % mechs)
+ return mechs
+
+ def do_sasl_first(self, mechanism, *args):
+ self.log(5, "Starting SASL authentication (%s) : %s" % (mechanism, ' '.join(args)))
+ ret = authenticate.do_sasl_first(mechanism, *args);
+ if ret['result'] == 'CONT':
+ self.log(5, "Need more SASL authentication : %r" % ret)
+ else:
+ self.log(5, "Finished SASL authentication : %r" % ret)
+ return ret
+
+ def do_sasl_next(self, b64_string):
+ self.log(5, "Continuing SASL authentication : %s" % b64_string)
+ ret = authenticate.do_sasl_next(b64_string);
+ if ret['result'] == 'CONT':
+ self.log(5, "Need more SASL authentication : %r" % ret)
+ else:
+ self.log(5, "Finished SASL authentication : %r" % ret)
+ return ret
+
def authenticate(self, username, passwd):
self.log(5, "Authenticating %s" % username)
self.params['username'] = username