1717
1818"""Command-line script that runs a server over roundup.cgi.client.
1919
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 $
2121"""
2222__docformat__ = 'restructuredtext'
2323
2424import errno , cgi , getopt , os , socket , sys , traceback , urllib , time
2525import ConfigParser , BaseHTTPServer , SocketServer , StringIO
2626
27+ try :
28+ from OpenSSL import SSL
29+ except ImportError :
30+ SSL = None
31+
2732# python version check
2833from roundup import configuration , version_check
2934from roundup import __version__ as roundup_version
6772 MULTIPROCESS_TYPES .append ("fork" )
6873DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES [- 1 ]
6974
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 )
150+
70151class RoundupRequestHandler (BaseHTTPServer .BaseHTTPRequestHandler ):
71152 TRACKER_HOMES = {}
72153 TRACKERS = None
@@ -406,6 +487,11 @@ class ServerConfig(configuration.Config):
406487 "Allowed values: %s." % ", " .join (MULTIPROCESS_TYPES )),
407488 (configuration .NullableFilePathOption , "template" , "" ,
408489 "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." ),
409495 )),
410496 ("trackers" , (), "Roundup trackers to serve.\n "
411497 "Each option in this section defines single Roundup tracker.\n "
@@ -426,7 +512,9 @@ class ServerConfig(configuration.Config):
426512 "nodaemon" : "D" ,
427513 "log_hostnames" : "N" ,
428514 "multiprocess" : "t:" ,
429- "template" : "i:" ,
515+ "template" : "i:" ,
516+ "ssl" : "s" ,
517+ "pem" : "e:" ,
430518 }
431519
432520 def __init__ (self , config_file = None ):
@@ -490,28 +578,38 @@ class RequestHandler(RoundupRequestHandler):
490578 DEBUG_MODE = self ["MULTIPROCESS" ] == "debug"
491579 CONFIG = self
492580
581+ if self ["SSL" ]:
582+ base_server = SecureHTTPServer
583+ else :
584+ base_server = BaseHTTPServer .HTTPServer
585+
493586 # obtain request server class
494587 if self ["MULTIPROCESS" ] not in MULTIPROCESS_TYPES :
495588 print _ ("Multiprocess mode \" %s\" is not available, "
496589 "switching to single-process" ) % self ["MULTIPROCESS" ]
497590 self ["MULTIPROCESS" ] = "none"
498- server_class = BaseHTTPServer . HTTPServer
591+ server_class = base_server
499592 elif self ["MULTIPROCESS" ] == "fork" :
500593 class ForkingServer (SocketServer .ForkingMixIn ,
501- BaseHTTPServer . HTTPServer ):
594+ base_server ):
502595 pass
503596 server_class = ForkingServer
504597 elif self ["MULTIPROCESS" ] == "thread" :
505598 class ThreadingServer (SocketServer .ThreadingMixIn ,
506- BaseHTTPServer . HTTPServer ):
599+ base_server ):
507600 pass
508601 server_class = ThreadingServer
509602 else :
510- server_class = BaseHTTPServer .HTTPServer
603+ server_class = base_server
604+
511605 # obtain server before changing user id - allows to
512606 # use port < 1024 if started as root
513607 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 )
515613 except socket .error , e :
516614 if e [0 ] == errno .EADDRINUSE :
517615 raise socket .error , \
@@ -608,7 +706,9 @@ def usage(message=''):
608706 -p <port> set the port to listen on (default: %(port)s)
609707 -l <fname> log to the file indicated by fname instead of stderr/stdout
610708 -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
612712 -t <mode> multiprocess mode (default: %(mp_def)s).
613713 Allowed values: %(mp_types)s.
614714%(os_part)s
@@ -811,4 +911,4 @@ def run(port=undefined, success_message=None):
811911if __name__ == '__main__' :
812912 run ()
813913
814- # vim: set filetype=python sts=4 sw=4 et si :
914+ # vim: sts=4 sw=4 et si
0 commit comments