Mercurial > p > roundup > code
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 |
