comparison 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
comparison
equal deleted inserted replaced
3882:46ef2a6fd79d 3883:679118b572d5
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 # 16 #
17 17
18 """Command-line script that runs a server over roundup.cgi.client. 18 """Command-line script that runs a server over roundup.cgi.client.
19 19
20 $Id: roundup_server.py,v 1.89 2007-09-02 16:05:36 jpend Exp $ 20 $Id: roundup_server.py,v 1.90 2007-09-03 17:20:07 jpend Exp $
21 """ 21 """
22 __docformat__ = 'restructuredtext' 22 __docformat__ = 'restructuredtext'
23 23
24 import errno, cgi, getopt, os, socket, sys, traceback, urllib, time 24 import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
25 import ConfigParser, BaseHTTPServer, SocketServer, StringIO 25 import ConfigParser, BaseHTTPServer, SocketServer, StringIO
26
27 try:
28 from OpenSSL import SSL
29 except ImportError:
30 SSL = None
26 31
27 # python version check 32 # python version check
28 from roundup import configuration, version_check 33 from roundup import configuration, version_check
29 from roundup import __version__ as roundup_version 34 from roundup import __version__ as roundup_version
30 35
64 else: 69 else:
65 MULTIPROCESS_TYPES.append("thread") 70 MULTIPROCESS_TYPES.append("thread")
66 if hasattr(os, 'fork'): 71 if hasattr(os, 'fork'):
67 MULTIPROCESS_TYPES.append("fork") 72 MULTIPROCESS_TYPES.append("fork")
68 DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1] 73 DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
74
75 def auto_ssl():
76 print _('WARNING: generating temporary SSL certificate')
77 import OpenSSL, time, random, sys
78 pkey = OpenSSL.crypto.PKey()
79 pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 768)
80 cert = OpenSSL.crypto.X509()
81 cert.set_serial_number(random.randint(0, sys.maxint))
82 cert.gmtime_adj_notBefore(0)
83 cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) # one year
84 cert.get_subject().CN = '*'
85 cert.get_subject().O = 'Roundup Dummy Certificate'
86 cert.get_issuer().CN = 'Roundup Dummy Certificate Authority'
87 cert.get_issuer().O = 'Self-Signed'
88 cert.set_pubkey(pkey)
89 cert.sign(pkey, 'md5')
90 ctx = SSL.Context(SSL.SSLv23_METHOD)
91 ctx.use_privatekey(pkey)
92 ctx.use_certificate(cert)
93
94 return ctx
95
96 class SecureHTTPServer(BaseHTTPServer.HTTPServer):
97 def __init__(self, server_address, HandlerClass, ssl_pem=None):
98 assert SSL, "pyopenssl not installed"
99 BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
100 self.socket = socket.socket(self.address_family, self.socket_type)
101 if ssl_pem:
102 ctx = SSL.Context(SSL.SSLv23_METHOD)
103 ctx.use_privatekey_file(ssl_pem)
104 ctx.use_certificate_file(ssl_pem)
105 else:
106 ctx = auto_ssl()
107 self.ssl_context = ctx
108 self.socket = SSL.Connection(ctx, self.socket)
109 self.server_bind()
110 self.server_activate()
111
112 def get_request(self):
113 (conn, info) = self.socket.accept()
114 if self.ssl_context:
115
116 class RetryingFile(object):
117 """ SSL.Connection objects can return Want__Error
118 on recv/write, meaning "try again". We'll handle
119 the try looping here """
120 def __init__(self, fileobj):
121 self.__fileobj = fileobj
122
123 def readline(self):
124 """ SSL.Connection can return WantRead """
125 line = None
126 while not line:
127 try:
128 line = self.__fileobj.readline()
129 except SSL.WantReadError:
130 line = None
131 return line
132
133 def __getattr__(self, attrib):
134 return getattr(self.__fileobj, attrib)
135
136 class ConnFixer(object):
137 """ wraps an SSL socket so that it implements makefile
138 which the HTTP handlers require """
139 def __init__(self, conn):
140 self.__conn = conn
141 def makefile(self, mode, bufsize):
142 fo = socket._fileobject(self.__conn, mode, bufsize)
143 return RetryingFile(fo)
144
145 def __getattr__(self, attrib):
146 return getattr(self.__conn, attrib)
147
148 conn = ConnFixer(conn)
149 return (conn, info)
69 150
70 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 151 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
71 TRACKER_HOMES = {} 152 TRACKER_HOMES = {}
72 TRACKERS = None 153 TRACKERS = None
73 LOG_IPADDRESS = 1 154 LOG_IPADDRESS = 1
404 (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS, 485 (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
405 "Set processing of each request in separate subprocess.\n" 486 "Set processing of each request in separate subprocess.\n"
406 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)), 487 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
407 (configuration.NullableFilePathOption, "template", "", 488 (configuration.NullableFilePathOption, "template", "",
408 "Tracker index template. If unset, built-in will be used."), 489 "Tracker index template. If unset, built-in will be used."),
490 (configuration.BooleanOption, "ssl", "no",
491 "Enable SSL support (requires pyopenssl)"),
492 (configuration.NullableFilePathOption, "pem", "",
493 "PEM file used for SSL. A temporary self-signed certificate\n"
494 "will be used if left blank."),
409 )), 495 )),
410 ("trackers", (), "Roundup trackers to serve.\n" 496 ("trackers", (), "Roundup trackers to serve.\n"
411 "Each option in this section defines single Roundup tracker.\n" 497 "Each option in this section defines single Roundup tracker.\n"
412 "Option name identifies the tracker and will appear in the URL.\n" 498 "Option name identifies the tracker and will appear in the URL.\n"
413 "Option value is tracker home directory path.\n" 499 "Option value is tracker home directory path.\n"
424 "logfile": "l:", 510 "logfile": "l:",
425 "pidfile": "d:", 511 "pidfile": "d:",
426 "nodaemon": "D", 512 "nodaemon": "D",
427 "log_hostnames": "N", 513 "log_hostnames": "N",
428 "multiprocess": "t:", 514 "multiprocess": "t:",
429 "template": "i:", 515 "template": "i:",
516 "ssl": "s",
517 "pem": "e:",
430 } 518 }
431 519
432 def __init__(self, config_file=None): 520 def __init__(self, config_file=None):
433 configuration.Config.__init__(self, config_file, self.SETTINGS) 521 configuration.Config.__init__(self, config_file, self.SETTINGS)
434 self.sections.append("trackers") 522 self.sections.append("trackers")
488 TRACKER_HOMES = dict(tracker_homes) 576 TRACKER_HOMES = dict(tracker_homes)
489 TRACKERS = trackers 577 TRACKERS = trackers
490 DEBUG_MODE = self["MULTIPROCESS"] == "debug" 578 DEBUG_MODE = self["MULTIPROCESS"] == "debug"
491 CONFIG = self 579 CONFIG = self
492 580
581 if self["SSL"]:
582 base_server = SecureHTTPServer
583 else:
584 base_server = BaseHTTPServer.HTTPServer
585
493 # obtain request server class 586 # obtain request server class
494 if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES: 587 if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
495 print _("Multiprocess mode \"%s\" is not available, " 588 print _("Multiprocess mode \"%s\" is not available, "
496 "switching to single-process") % self["MULTIPROCESS"] 589 "switching to single-process") % self["MULTIPROCESS"]
497 self["MULTIPROCESS"] = "none" 590 self["MULTIPROCESS"] = "none"
498 server_class = BaseHTTPServer.HTTPServer 591 server_class = base_server
499 elif self["MULTIPROCESS"] == "fork": 592 elif self["MULTIPROCESS"] == "fork":
500 class ForkingServer(SocketServer.ForkingMixIn, 593 class ForkingServer(SocketServer.ForkingMixIn,
501 BaseHTTPServer.HTTPServer): 594 base_server):
502 pass 595 pass
503 server_class = ForkingServer 596 server_class = ForkingServer
504 elif self["MULTIPROCESS"] == "thread": 597 elif self["MULTIPROCESS"] == "thread":
505 class ThreadingServer(SocketServer.ThreadingMixIn, 598 class ThreadingServer(SocketServer.ThreadingMixIn,
506 BaseHTTPServer.HTTPServer): 599 base_server):
507 pass 600 pass
508 server_class = ThreadingServer 601 server_class = ThreadingServer
509 else: 602 else:
510 server_class = BaseHTTPServer.HTTPServer 603 server_class = base_server
604
511 # obtain server before changing user id - allows to 605 # obtain server before changing user id - allows to
512 # use port < 1024 if started as root 606 # use port < 1024 if started as root
513 try: 607 try:
514 httpd = server_class((self["HOST"], self["PORT"]), RequestHandler) 608 args = ((self["HOST"], self["PORT"]), RequestHandler)
609 kwargs = {}
610 if self["SSL"]:
611 kwargs['ssl_pem'] = self["PEM"]
612 httpd = server_class(*args, **kwargs)
515 except socket.error, e: 613 except socket.error, e:
516 if e[0] == errno.EADDRINUSE: 614 if e[0] == errno.EADDRINUSE:
517 raise socket.error, \ 615 raise socket.error, \
518 _("Unable to bind to port %s, port already in use.") \ 616 _("Unable to bind to port %s, port already in use.") \
519 % self["PORT"] 617 % self["PORT"]
606 -C <fname> use configuration file <fname> 704 -C <fname> use configuration file <fname>
607 -n <name> set the host name of the Roundup web server instance 705 -n <name> set the host name of the Roundup web server instance
608 -p <port> set the port to listen on (default: %(port)s) 706 -p <port> set the port to listen on (default: %(port)s)
609 -l <fname> log to the file indicated by fname instead of stderr/stdout 707 -l <fname> log to the file indicated by fname instead of stderr/stdout
610 -N log client machine names instead of IP addresses (much slower) 708 -N log client machine names instead of IP addresses (much slower)
611 -i set tracker index template 709 -i <fname> set tracker index template
710 -s enable SSL
711 -e <fname> PEM file containing SSL key and certificate
612 -t <mode> multiprocess mode (default: %(mp_def)s). 712 -t <mode> multiprocess mode (default: %(mp_def)s).
613 Allowed values: %(mp_types)s. 713 Allowed values: %(mp_types)s.
614 %(os_part)s 714 %(os_part)s
615 715
616 Long options: 716 Long options:
809 print 'Keyboard Interrupt: exiting' 909 print 'Keyboard Interrupt: exiting'
810 910
811 if __name__ == '__main__': 911 if __name__ == '__main__':
812 run() 912 run()
813 913
814 # vim: set filetype=python sts=4 sw=4 et si : 914 # vim: sts=4 sw=4 et si

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