diff roundup/scripts/roundup_server.py @ 3883:679118b572d5

add SSL to roundup-server via pyopenssl If pyopenssl is installed you can enable SSL support in roundup-server through two new config options. new command line options: -s enable SSL -e specify key/cert PEM (optional) If no key/cert is specified a warning is printed and a temporary, self-signed key+cert is generated for you. Updated docs for all this.
author Justus Pendleton <jpend@users.sourceforge.net>
date Mon, 03 Sep 2007 17:20:07 +0000
parents 2359d6304a4f
children 1165f2204542
line wrap: on
line diff
--- a/roundup/scripts/roundup_server.py	Mon Sep 03 17:14:09 2007 +0000
+++ b/roundup/scripts/roundup_server.py	Mon Sep 03 17:20:07 2007 +0000
@@ -17,13 +17,18 @@
 
 """Command-line script that runs a server over roundup.cgi.client.
 
-$Id: roundup_server.py,v 1.89 2007-09-02 16:05:36 jpend Exp $
+$Id: roundup_server.py,v 1.90 2007-09-03 17:20:07 jpend Exp $
 """
 __docformat__ = 'restructuredtext'
 
 import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
 import ConfigParser, BaseHTTPServer, SocketServer, StringIO
 
+try:
+    from OpenSSL import SSL
+except ImportError:
+    SSL = None
+
 # python version check
 from roundup import configuration, version_check
 from roundup import __version__ as roundup_version
@@ -67,6 +72,82 @@
     MULTIPROCESS_TYPES.append("fork")
 DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
 
+def auto_ssl():
+    print _('WARNING: generating temporary SSL certificate')
+    import OpenSSL, time, random, sys
+    pkey = OpenSSL.crypto.PKey()
+    pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 768)
+    cert = OpenSSL.crypto.X509()
+    cert.set_serial_number(random.randint(0, sys.maxint))
+    cert.gmtime_adj_notBefore(0)
+    cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) # one year
+    cert.get_subject().CN = '*'
+    cert.get_subject().O = 'Roundup Dummy Certificate'
+    cert.get_issuer().CN = 'Roundup Dummy Certificate Authority'
+    cert.get_issuer().O = 'Self-Signed'
+    cert.set_pubkey(pkey)
+    cert.sign(pkey, 'md5')
+    ctx = SSL.Context(SSL.SSLv23_METHOD)
+    ctx.use_privatekey(pkey)
+    ctx.use_certificate(cert)
+
+    return ctx
+
+class SecureHTTPServer(BaseHTTPServer.HTTPServer):
+    def __init__(self, server_address, HandlerClass, ssl_pem=None):
+        assert SSL, "pyopenssl not installed"
+        BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
+        self.socket = socket.socket(self.address_family, self.socket_type)
+        if ssl_pem:
+            ctx = SSL.Context(SSL.SSLv23_METHOD)
+            ctx.use_privatekey_file(ssl_pem)
+            ctx.use_certificate_file(ssl_pem)
+        else:
+            ctx = auto_ssl()
+        self.ssl_context = ctx
+        self.socket = SSL.Connection(ctx, self.socket)
+        self.server_bind()
+        self.server_activate()
+
+    def get_request(self):
+        (conn, info) = self.socket.accept()
+        if self.ssl_context:
+
+            class RetryingFile(object):
+                """ SSL.Connection objects can return Want__Error
+                    on recv/write, meaning "try again". We'll handle
+                    the try looping here """
+                def __init__(self, fileobj):
+                    self.__fileobj = fileobj
+
+                def readline(self):
+                    """ SSL.Connection can return WantRead """
+                    line = None
+                    while not line:
+                        try:
+                            line = self.__fileobj.readline()
+                        except SSL.WantReadError:
+                            line = None
+                    return line
+
+                def __getattr__(self, attrib):
+                    return getattr(self.__fileobj, attrib)
+
+            class ConnFixer(object):
+                """ wraps an SSL socket so that it implements makefile
+                    which the HTTP handlers require """
+                def __init__(self, conn):
+                    self.__conn = conn
+                def makefile(self, mode, bufsize):
+                    fo = socket._fileobject(self.__conn, mode, bufsize)
+                    return RetryingFile(fo)
+
+                def __getattr__(self, attrib):
+                    return getattr(self.__conn, attrib)
+
+            conn = ConnFixer(conn)
+        return (conn, info)
+
 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     TRACKER_HOMES = {}
     TRACKERS = None
@@ -406,6 +487,11 @@
                 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
             (configuration.NullableFilePathOption, "template", "",
                 "Tracker index template. If unset, built-in will be used."),
+            (configuration.BooleanOption, "ssl", "no",
+                "Enable SSL support (requires pyopenssl)"),
+            (configuration.NullableFilePathOption, "pem", "",
+                "PEM file used for SSL. A temporary self-signed certificate\n"
+                "will be used if left blank."),
         )),
         ("trackers", (), "Roundup trackers to serve.\n"
             "Each option in this section defines single Roundup tracker.\n"
@@ -426,7 +512,9 @@
         "nodaemon": "D",
         "log_hostnames": "N",
         "multiprocess": "t:",
-        "template": "i:",
+	"template": "i:",
+	"ssl": "s",
+	"pem": "e:",
     }
 
     def __init__(self, config_file=None):
@@ -490,28 +578,38 @@
             DEBUG_MODE = self["MULTIPROCESS"] == "debug"
             CONFIG = self
 
+        if self["SSL"]:
+            base_server = SecureHTTPServer
+        else:
+            base_server = BaseHTTPServer.HTTPServer
+
         # obtain request server class
         if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
             print _("Multiprocess mode \"%s\" is not available, "
                 "switching to single-process") % self["MULTIPROCESS"]
             self["MULTIPROCESS"] = "none"
-            server_class = BaseHTTPServer.HTTPServer
+            server_class = base_server
         elif self["MULTIPROCESS"] == "fork":
             class ForkingServer(SocketServer.ForkingMixIn,
-                BaseHTTPServer.HTTPServer):
+                base_server):
                     pass
             server_class = ForkingServer
         elif self["MULTIPROCESS"] == "thread":
             class ThreadingServer(SocketServer.ThreadingMixIn,
-                BaseHTTPServer.HTTPServer):
+                base_server):
                     pass
             server_class = ThreadingServer
         else:
-            server_class = BaseHTTPServer.HTTPServer
+            server_class = base_server
+
         # obtain server before changing user id - allows to
         # use port < 1024 if started as root
         try:
-            httpd = server_class((self["HOST"], self["PORT"]), RequestHandler)
+            args = ((self["HOST"], self["PORT"]), RequestHandler)
+            kwargs = {}
+            if self["SSL"]:
+                kwargs['ssl_pem'] = self["PEM"]
+            httpd = server_class(*args, **kwargs)
         except socket.error, e:
             if e[0] == errno.EADDRINUSE:
                 raise socket.error, \
@@ -608,7 +706,9 @@
  -p <port>     set the port to listen on (default: %(port)s)
  -l <fname>    log to the file indicated by fname instead of stderr/stdout
  -N            log client machine names instead of IP addresses (much slower)
- -i            set tracker index template
+ -i <fname>    set tracker index template
+ -s            enable SSL
+ -e <fname>    PEM file containing SSL key and certificate
  -t <mode>     multiprocess mode (default: %(mp_def)s).
                Allowed values: %(mp_types)s.
 %(os_part)s
@@ -811,4 +911,4 @@
 if __name__ == '__main__':
     run()
 
-# vim: set filetype=python sts=4 sw=4 et si :
+# vim: sts=4 sw=4 et si

Roundup Issue Tracker: http://roundup-tracker.org/