Skip to content

Commit bf17fed

Browse files
author
Justus Pendleton
committed
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.
1 parent dfa36cd commit bf17fed

File tree

4 files changed

+129
-10
lines changed

4 files changed

+129
-10
lines changed

doc/admin_guide.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Administration Guide
33
====================
44

5-
:Version: $Revision: 1.24 $
5+
:Version: $Revision: 1.25 $
66

77
.. contents::
88

@@ -83,6 +83,8 @@ The basic configuration file layout is as follows (take from the
8383
;pidfile =
8484
;logfile =
8585
;template =
86+
;ssl = no
87+
;pem =
8688

8789
[trackers]
8890
; Add one of these per tracker being served
@@ -114,6 +116,14 @@ are as follows:
114116
Specifies a template used for displaying the tracker index when
115117
multiple trackers are being used. The variable "trackers" is available
116118
to the template and is a dict of all configured trackers.
119+
**ssl**
120+
Enables the use of SSL to secure the connection to the roundup-server.
121+
If you enable this, ensure that your tracker's config.ini specifies
122+
an *https* URL.
123+
**pem**
124+
If specified, the SSL PEM file containing the private key and certificate.
125+
If not specified, roundup will generate a temporary, self-signed certificate
126+
for use.
117127
**trackers** section
118128
Each line denotes a mapping from a URL component to a tracker home.
119129
Make sure the name part doesn't include any url-unsafe characters like

doc/roundup-server.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ Sets a filename to use as a template for generating the tracker index page.
2626
The variable "trackers" is available to the template and is a dict of all
2727
configured trackers.
2828
.TP
29+
\fB-s\fP
30+
Enables to use of SSL.
31+
.TP
32+
\fB-e\fP \fIfile\fP
33+
Sets a filename containing the PEM file to use for SSL. If left blank, a
34+
temporary self-signed certificate will be used.
35+
.TP
2936
\fB-h\fP
3037
print help
3138
.TP

doc/roundup-server.ini.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ port = 8080
99
;pidfile =
1010
;logfile =
1111
;template =
12+
;ssl = no
13+
;pem =
1214

1315

1416
; Add one of these per tracker being served

roundup/scripts/roundup_server.py

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@
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

2424
import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
2525
import ConfigParser, BaseHTTPServer, SocketServer, StringIO
2626

27+
try:
28+
from OpenSSL import SSL
29+
except ImportError:
30+
SSL = None
31+
2732
# python version check
2833
from roundup import configuration, version_check
2934
from roundup import __version__ as roundup_version
@@ -67,6 +72,82 @@
6772
MULTIPROCESS_TYPES.append("fork")
6873
DEFAULT_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+
70151
class 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):
811911
if __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

Comments
 (0)