-----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