-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

See the attached patch. This updates tmda-ofmipd to directly support SSL
without requiring the use of stunnel etc. It requires the tlslite Python
module from http://trevp.net/tlslite/.

Note that right now, only "SMTP over SSL" is supported; I haven't
attempted to get STARTTLS working yet.

I've tested the regular TMDA proxy in both daemon and "one session"
mode. I don't have the ability to test the "VDomain" proxy (-S option).
All testing with tlslite-0.3.8, python 2.3.4, x86 arch, Fedora Core 3
OS. I'm not sure right now if I have installed any of the "speedup"
packages that tlslite can use.

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.2 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iD8DBQFF4UvOhk3bo0lNTrURAntJAJ9eBwF1MtwWsNYVZj52RcUNWsu1OgCgvMmU
oB76iqKxQLypeMQznOyaruw=
=26t8
-----END PGP SIGNATURE-----
Index: bin/tmda-ofmipd
===================================================================
--- bin/tmda-ofmipd     (revision 2157)
+++ bin/tmda-ofmipd     (working copy)
@@ -174,6 +174,33 @@
 send mail.  If the script returns a 0, the message is allowed.  For
 any other value, the message is rejected.""")
 
+congroup.add_option("", "--ssl",
+                 action="store_true", default=False, dest="ssl",
+                 help= \
+"""Enable SSL encryption. This mode immediately initiates the SSL/TLS
+protocol as soon as a connection is made. This mode is not support
+for the STARTTLS command. This configuration is typically run on
+port 465 (smtps).""")
+
+# FIXME: Enable this once STARTTLS is implemented
+#congroup.add_option("", "--tls",
+#                  action="store_true", default=False, dest="tls",
+#                  help= \
+#"""Enable TLS mode, i.e. the STARTTLS command. With this option
+#enabled, all AUTH commands are disabled until after STARTTLS has
+#been issued. This configuration is typically run on port 587
+#(submission).""")
+
+congroup.add_option("", "--ssl-cert",
+                  metavar="/PATH/TO/FILE", dest="ssl_cert",
+                  help= \
+"""Location of the SSL/TLS certificate key file.""")
+
+congroup.add_option("", "--ssl-key",
+                  metavar="/PATH/TO/FILE", dest="ssl_key",
+                  help= \
+"""Location of the SSL/TLS private key file.""")
+
 # authentication
 authgroup.add_option("-R", "--remoteauth",
                  metavar="PROTO://HOST[:PORT][/DN]", dest="remoteauth",
@@ -251,6 +278,9 @@
 
 (opts, args) = parser.parse_args()
 
+# FIXME: Remove this once STARTTLS is implemented:
+opts.tls = False
+
 if opts.full_version:
     print Version.ALL
     sys.exit()
@@ -359,6 +389,24 @@
         raise ValueError
 
 
+if opts.tls or opts.ssl:
+    if opts.tls and opts.ssl:
+        raise ValueError, 'Can\'t do TLS and SSL at the same time'
+
+    from tlslite.api import *
+
+    fhc = file(opts.ssl_cert, 'r')
+    datac = fhc.read()
+    fhc.close()
+    x509 = X509()
+    x509.parse(datac)
+    opts.ssl_cert_value = X509CertChain([x509])
+
+    fhk = file(opts.ssl_key, 'r')
+    datak = fhk.read()
+    fhk.close()
+    opts.ssl_key_value = parsePEMKey(datak, private=True)
+
 # Utility functions
 def pipecmd(command, *strings):
     popen2._cleanup()
@@ -508,13 +556,23 @@
 
 # Classes
 
-class SMTPChannel(asynchat.async_chat):
+class SMTPSession(asynchat.async_chat):
     COMMAND = 0
     DATA = 1
     AUTH = 2
+
+    ac_in_buffer_size = 16384
     
-    def __init__(self, server, conn):
+    def __init__(self, conn, opts):
+        if opts.ssl:
+            self.__class__.__bases__ = (TLSAsyncDispatcherMixIn,) + 
self.__class__.__bases__
+            TLSAsyncDispatcherMixIn.__init__(self, conn, asynchat.async_chat)
+            self.tlsConnection.ignoreAbruptClose = True
+            self.setServerHandshakeOp(certChain=opts.ssl_cert_value,
+                                      privateKey=opts.ssl_key_value)
+
         asynchat.async_chat.__init__(self, conn)
+
         # SMTP AUTH
         self.__smtpauth = 0
         self.__auth_resp1 = None
@@ -529,7 +587,6 @@
             self.__sasl_types.remove('cram-md5')
         self.__auth_cram_md5_ticket = '<[EMAIL PROTECTED]>' % 
(random.randrange(10000),
                                                       int(time.time()), FQDN)
-        self.__server = server
         self.__conn = conn
         self.__line = []
         self.__state = self.COMMAND
@@ -591,6 +648,96 @@
     def collect_incoming_data(self, data):
         self.__line.append(data)
 
+    # Implementation of base class abstract method
+    def found_terminator(self):
+        line = EMPTYSTRING.join(self.__line)
+        if opts.debug:
+            print >> DEBUGSTREAM, 'Data:', repr(line)
+        self.__line = []
+        if self.__state == self.COMMAND:
+            if not line:
+                self.push('500 Error: bad syntax')
+                return
+            method = None
+            i = line.find(' ')
+            if i < 0:
+                command = line.upper()
+                arg = None
+            else:
+                command = line[:i].upper()
+                arg = line[i+1:].strip()
+            method = getattr(self, 'smtp_' + command, None)
+            if not method:
+                self.push('502 Error: command "%s" not implemented' % command)
+                return
+            method(arg)
+            return
+        elif self.__state == self.DATA:
+            # Remove extraneous carriage returns and de-transparency according
+            # to RFC 2821, Section 4.5.2.
+            data = []
+            for text in line.split('\r\n'):
+                if text and text[0] == '.':
+                    data.append(text[1:])
+                else:
+                    data.append(text)
+            self.__data = NEWLINE.join(data)
+
+            if not opts.throttlescript or not os.system("%s %s" % 
(opts.throttlescript,
+                self.__auth_username)):
+                try:
+                    status = self.process_message(self.__peer,
+                                                  self.__mailfrom,
+                                                  self.__rcpttos,
+                                                  self.__data,
+                                                  self.__auth_username)
+                except:
+                    print >>DEBUGSTREAM, "process_message raised an exception:"
+                    import traceback
+                    traceback.print_exc(DEBUGSTREAM)
+                    raise
+            else:
+                status = self.push('450 Outgoing mail quota exceeded')
+
+            self.__rcpttos = []
+            self.__mailfrom = None
+            self.__state = self.COMMAND
+            self.set_terminator('\r\n')
+            if not status:
+                self.push('250 Ok')
+            else:
+                self.push(status)
+        elif self.__state == self.AUTH:
+            if line == '*':
+                # client canceled the authentication attempt
+                self.push('501 AUTH exchange cancelled')
+                self.auth_reset_state()
+                return
+            if not self.__auth_resp1:
+                self.__auth_resp1 = line
+            else:
+                self.__auth_resp2 = line
+            self.auth_challenge()
+        else:
+            self.push('451 Internal confusion')
+            return
+
+    # factored
+    def __getaddr(self, keyword, arg):
+        address = None
+        keylen = len(keyword)
+        if arg[:keylen].upper() == keyword:
+            address = arg[keylen:].strip()
+            if not address:
+                pass
+            elif address[0] == '<' and address[-1] == '>' and address <> '<>':
+                # Addresses can be in the form <[EMAIL PROTECTED]> but watch 
out
+                # for null address, e.g. <>
+                address = address[1:-1]
+        return address
+
+    # Authentication methods
+
     def verify_login(self, b64username, b64password):
         """The LOGIN SMTP authentication method is an undocumented,
         unstandardized Microsoft invention.  Needed to support MS
@@ -751,80 +898,6 @@
             self.auth_verify()
             return
 
-    # Implementation of base class abstract method
-    def found_terminator(self):
-        line = EMPTYSTRING.join(self.__line)
-        if opts.debug:
-            print >> DEBUGSTREAM, 'Data:', repr(line)
-        self.__line = []
-        if self.__state == self.COMMAND:
-            if not line:
-                self.push('500 Error: bad syntax')
-                return
-            method = None
-            i = line.find(' ')
-            if i < 0:
-                command = line.upper()
-                arg = None
-            else:
-                command = line[:i].upper()
-                arg = line[i+1:].strip()
-            method = getattr(self, 'smtp_' + command, None)
-            if not method:
-                self.push('502 Error: command "%s" not implemented' % command)
-                return
-            method(arg)
-            return
-        elif self.__state == self.DATA:
-            # Remove extraneous carriage returns and de-transparency according
-            # to RFC 2821, Section 4.5.2.
-            data = []
-            for text in line.split('\r\n'):
-                if text and text[0] == '.':
-                    data.append(text[1:])
-                else:
-                    data.append(text)
-            self.__data = NEWLINE.join(data)
-
-            if not opts.throttlescript or not os.system("%s %s" % 
(opts.throttlescript,
-                self.__auth_username)):
-                try:
-                    status = self.__server.process_message(self.__peer,
-                                                           self.__mailfrom,
-                                                           self.__rcpttos,
-                                                           self.__data,
-                                                           
self.__auth_username)
-                except:
-                    print >>DEBUGSTREAM, "process_message raised an exception:"
-                    import traceback
-                    traceback.print_exc(DEBUGSTREAM)
-                    raise
-            else:
-                status = self.push('450 Outgoing mail quota exceeded')
-
-            self.__rcpttos = []
-            self.__mailfrom = None
-            self.__state = self.COMMAND
-            self.set_terminator('\r\n')
-            if not status:
-                self.push('250 Ok')
-            else:
-                self.push(status)
-        elif self.__state == self.AUTH:
-            if line == '*':
-                # client canceled the authentication attempt
-                self.push('501 AUTH exchange cancelled')
-                self.auth_reset_state()
-                return
-            if not self.__auth_resp1:
-                self.__auth_resp1 = line
-            else:
-                self.__auth_resp2 = line
-            self.auth_challenge()
-        else:
-            self.push('451 Internal confusion')
-            return
-
     # ESMTP/SMTP commands
 
     def smtp_EHLO(self, arg):
@@ -844,6 +917,10 @@
             rh.append('(%s [%s])' % (self.__peername, self.__peerip))
         else:
             rh.append('(%s)' % (self.__peerip))
+        if opts.ssl:
+            rh.append('(using SMTP over TLS)')
+        if opts.tls:
+            rh.append('(using STARTTLS)')
         rh.append('by %s (tmda-ofmipd) with ESMTP;' % (self.__fqdn))
         rh.append(Util.make_date())
         os.environ['TMDA_OFMIPD_RECEIVED'] = ' '.join(rh)
@@ -859,20 +936,6 @@
         self.push('221 Bye')
         self.close_when_done()
 
-    # factored
-    def __getaddr(self, keyword, arg):
-        address = None
-        keylen = len(keyword)
-        if arg[:keylen].upper() == keyword:
-            address = arg[keylen:].strip()
-            if not address:
-                pass
-            elif address[0] == '<' and address[-1] == '>' and address <> '<>':
-                # Addresses can be in the form <[EMAIL PROTECTED]> but watch 
out
-                # for null address, e.g. <>
-                address = address[1:-1]
-        return address
-
     def smtp_MAIL(self, arg):
         # Authentication required first
         if not self.__smtpauth:
@@ -947,12 +1010,7 @@
         self.__state = self.AUTH
         self.auth_challenge()
 
-
-class MessageProcessor:
-    """Base 'pure' SMTP message processing class.
-    Raises NotImplementedError if you try to use it."""
-
-    # API for "doing something useful with the message"
+    # Abstract API for "doing something useful with the message"
     def process_message(self, peer, mailfrom, rcpttos, data):
         """Override this abstract method to handle messages from the client.
 
@@ -978,7 +1036,7 @@
         raise NotImplementedError
 
 
-class DebuggingMessageProcessor(MessageProcessor):
+class DebuggingSession(SMTPSession):
     """Simply prints each message it receives on stdout."""
     # Do something with the gathered message
     def process_message(self, peer, mailfrom, rcpttos, data):
@@ -994,43 +1052,13 @@
         print '------------ END MESSAGE ------------'
 
 
-class SMTPServer(asyncore.dispatcher, MessageProcessor):
-    """Run an SMTP server daemon - accept new socket connections and
-    process SMTP sessions on each conneciton."""
-    def __init__(self, localaddr, remoteaddr):
-        self._localaddr = localaddr
-        self._remoteaddr = remoteaddr
-        asyncore.dispatcher.__init__(self)
-        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
-        # try to re-use a server port if possible
-        self.set_reuse_addr()
-        self.bind(localaddr)
-        self.listen(5)
-        print >> DEBUGSTREAM, \
-              'tmda-ofmipd started at %s\n\tListening on %s' % \
-              (Util.make_date(), opts.proxyport)
+class PureProxySession(SMTPSession):
+    def __init__(self, conn, opts):
+        self.SMTPSession.__init__(conn, opts)
+        # FIXME: This isn't set anywhere yet.
+        # But, since PureProxySession isn't used anywhere, that's not an issue 
right now...
+        self._remoteaddr = opts.remoteaddr
 
-    def readable(self):
-        if len(asyncore.socket_map) > opts.connections:
-            # too many simultaneous connections
-            return 0
-        else:
-            return 1
-        
-    def handle_accept(self):
-        conn = self.accept()[0]
-        self._channel = SMTPChannel(self, conn)
-
-
-class SMTPProcessor(asyncore.dispatcher, MessageProcessor):
-    """Run a single SMTP session, on the stdin file descriptor."""
-    def __init__(self):
-        asyncore.dispatcher.__init__(self)
-        conn = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
-        self._channel = SMTPChannel(self, conn)
-
-
-class PureProxy(MessageProcessor):
     """Proxies all messages to a real smtpd which does final delivery.
     Used solely as a base class."""
     def process_message(self, peer, mailfrom, rcpttos, data):
@@ -1072,7 +1100,7 @@
         return refused
 
 
-class VDomainProxy(PureProxy):
+class VDomainSession(SMTPSession):
     """This proxy is used only for virtual domain support in a qmail +
     (VPopMail or VMailMgr) environment.  It needs to behave differently from
     the standard TMDA proxy in that authenticated users are not system
@@ -1142,7 +1170,7 @@
             os._exit(0)
 
 
-class TMDAProxy(PureProxy):
+class TMDASession(SMTPSession):
     """Using this server for outgoing smtpd, the authenticated user
     will have his mail tagged using his TMDA config file."""
     def process_message(self, peer, mailfrom, rcpttos, data, auth_username):
@@ -1190,34 +1218,40 @@
             Util.pipecmd(inject_cmd, data)
             
 
-class VDomainProxyServer(VDomainProxy, SMTPServer):
-    """A proxy server class that binds to the server port, accepts new
-    connections, and processes them as necessary for a qmail virtual user
-    environment. All implementation is inherited from superclasses."""
-    pass
+class SMTPServer(asyncore.dispatcher):
+    """Run an SMTP server daemon - accept new socket connections and
+    process SMTP sessions on each connection."""
+    def __init__(self, localaddr, session_class, opts):
+        self._localaddr = localaddr
+        self._session_class = session_class
+        self._opts = opts
+        asyncore.dispatcher.__init__(self)
+        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        # try to re-use a server port if possible
+        self.set_reuse_addr()
+        self.bind(localaddr)
+        self.listen(5)
+        print >> DEBUGSTREAM, \
+              'tmda-ofmipd started at %s\n\tListening on %s:%d' % \
+              (Util.make_date(), localaddr[0], localaddr[1])
 
+    def readable(self):
+        if len(asyncore.socket_map) > opts.sessions:
+            # too many simultaneous sessions
+            return 0
+        else:
+            return 1
 
-class VDomainProxyProcessor(VDomainProxy, SMTPProcessor):
-    """A proxy server class that handles an SMTP session on a previously
-    created socket, and performs processing as necessary for a qmail virtual
-    user environment. All implementation is inherited from superclasses."""
-    pass
+    def handle_accept(self):
+        conn = self.accept()[0]
+        self._channel = self._session_class(conn, self._opts)
 
 
-class TMDAProxyServer(TMDAProxy, SMTPServer):
-    """A proxy server class that binds to the server port, accepts new
-    connections, and processes them as necessary for a system user
-    environment. All implementation is inherited from superclasses."""
-    pass
+def smtp_session_of_stdin(session_class, opts):
+    conn = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
+    return session_class(conn, opts)
+ 
 
-
-class TMDAProxyProcessor(TMDAProxy, SMTPProcessor):
-    """A proxy server class that handles an SMTP session on a previously
-    created socket, and performs processing as necessary for a system
-    user environment. All implementation is inherited from superclasses."""
-    pass
-
-
 def main():
     # check permissions of authfile if using only remote
     # authentication.
@@ -1226,20 +1260,19 @@
         if authfile_mode not in (400, 600):
             raise IOError, \
                opts.authfile + ' must be chmod 400 or 600!'
-    # try binding to the specified host:port
-    host, port = opts.proxyport.split(':', 1)
+
     if opts.vhomescript:
-        if opts.one_session:
-            proxy = VDomainProxyProcessor()
-        else:
-            proxy = VDomainProxyServer((host, int(port)),
-                                       ('localhost', 25))
+        session_class = VDomainSession
     else:
-        if opts.one_session:
-            proxy = TMDAProxyProcessor()
-        else:
-            proxy = TMDAProxyServer((host, int(port)),
-                                    ('localhost', 25))
+        session_class = TMDASession
+
+    if opts.one_session:
+        proxy = smtp_session_of_stdin(session_class, opts)
+    else:
+        # try binding to the specified host:port
+        host, port = opts.proxyport.split(':', 1)
+        proxy = SMTPServer((host, int(port)), session_class, opts)
+
     if running_as_root:
         pw_uid = Util.getuid(opts.username)
         # check ownership of authfile if using only remote
Index: bin/ChangeLog
===================================================================
--- bin/ChangeLog       (revision 2157)
+++ bin/ChangeLog       (working copy)
@@ -1,3 +1,11 @@
+2007-02-25  Stephen Warren  <[EMAIL PROTECTED]>
+
+       * tmda-ofmipd: Implemented --ssl, --ssl-key, --ssl-crt options.
+       These implement SSL (not yet STARTTLS) support directly in tmda-
+       ofmipd, without requiring the use of stunnel etc.
+       These options require the tlslite Python module to be installed.
+       See http://trevp.net/tlslite/.
+
 2007-02-23  Stephen Warren  <[EMAIL PROTECTED]>
 
        * tmda-rfilter: Add fix from Bernard Johnson that enables multiple
_________________________________________________
tmda-workers mailing list ([email protected])
http://tmda.net/lists/listinfo/tmda-workers

Reply via email to