index 8dab82ead8c828bbe017bad2b767c9b7328410ce..2ebd56ea3c79e3bb26f8b3ea0df00d0bd573eb6a 100644 (file)
# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-#
-""" HTTP Server that serves roundup.
+#
+
+"""Command-line script that runs a server over roundup.cgi.client.
-$Id: roundup_server.py,v 1.28 2003-10-05 23:29:49 richard Exp $
+$Id: roundup_server.py,v 1.94 2007-09-25 04:27:12 jpend Exp $
"""
+__docformat__ = 'restructuredtext'
-# python version check
-from roundup import version_check
+import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
+import ConfigParser, BaseHTTPServer, SocketServer, StringIO
+
+try:
+ from OpenSSL import SSL
+except ImportError:
+ SSL = None
+
+from time import sleep
-import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
-import BaseHTTPServer, socket
+# python version check
+from roundup import configuration, version_check
+from roundup import __version__ as roundup_version
# Roundup modules of use here
from roundup.cgi import cgitb, client
+from roundup.cgi.PageTemplates.PageTemplate import PageTemplate
import roundup.instance
from roundup.i18n import _
-#
-## Configuration
-#
-
-# This indicates where the Roundup trackers live. They're given as NAME ->
-# TRACKER_HOME, where the NAME part is used in the URL to select the
-# appropriate reacker.
-# Make sure the NAME part doesn't include any url-unsafe characters like
-# spaces, as these confuse the cookie handling in browsers like IE.
-TRACKER_HOMES = {
-# 'example': '/path/to/example',
-}
-
-ROUNDUP_USER = None
-
-
-#
-## end configuration
-#
-
+# "default" favicon.ico
+# generate by using "icotool" and tools/base64
import zlib, base64
favico = zlib.decompress(base64.decodestring('''
-eJyVUk2IQVEUfn4yaRYjibdQZiVba/ZE2djIUmHWFjaKGVmIlY2iFMVG2ViQhXqFSP6iFFJvw4uF
-LGdWd743mpeMn+a88917Oue7955z3qEoET6FQkHx8iahKDV2A8B7XgERRf/EKMSUzyf8ypbbnnQy
-mWBdr9eVSkVw3tJGoxGNRpvNZigUyufzWPv9Pvwcx0UiERj7/V4g73Y7j8fTarWMRmO73U4kEkKI
-YZhardbr9eLxuOD0+/2ZTMZisYjFYpqmU6kU799uN5tNMBg8HA7ZbPY8GaTh8/mEipRKpclk0ul0
-NpvNarUmk0mWZS/yr9frcrmc+iMOh+NWydPp1Ov1SiSSc344HL7fKKfTiSN2u12tVqOcxWJxn6/V
-ag0GAwxkrlKp5vP5fT7ulMlk6XRar9dLpVIUXi6Xb5Hxa1wul0ajKZVKsVjM7XYXCoVOp3OVPJvN
-AoFAtVo1m825XO7hSODOYrH4kHbxxGAwwODBGI/H6DBs5LNara7yl8slGjIcDsHpdrunU6PRCAP2
-r3fPdUcIYeyEfLSAJ0LeAUZHCAt8Al/8/kLIEWDB5YDj0wm8fAP6fVfo
+eJztjr1PmlEUh59XgVoshdYPWorFIhaRFq0t9pNq37b60lYSTRzcTFw6GAfj5gDYaF0dTB0MxMSE
+gQQd3FzKJiEC0UCIUUN1M41pV2JCXySg/0ITn5tfzvmdc+85FwT56HSc81UJjXJsk1UsNcsSqCk1
+BS64lK+vr7OyssLJyQl2ux2j0cjU1BQajYZIJEIwGMRms+H3+zEYDExOTjI2Nsbm5iZWqxWv18vW
+1hZDQ0Ok02kmJiY4Ojpienqa3d1dxsfHUSqVeDwe5ufnyeVyrK6u4nK5ODs7Y3FxEYfDwdzcHCaT
+icPDQ5LJJIIgMDIyQj6fZ39/n+3tbdbW1pAkiYWFBWZmZtjb2yMejzM8PEwgEMDn85HNZonFYqjV
+asLhMMvLy2QyGfR6PaOjowwODmKxWDg+PkalUhEKhSgUCiwtLWE2m9nZ2UGhULCxscHp6SmpVIpo
+NMrs7CwHBwdotVoSiQRXXPG/IzY7RHtt922xjFRb01H1XhKfPBNbi/7my7rrLXJ88eppvxwEfV3f
+NY3Y6exofVdsV3+2wnPFDdPjB83n7xuVpcFvygPbGwxF31LZIKrQDfR2Xvh7lmrX654L/7bvlnng
+bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g
+23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI=
'''.strip()))
+DEFAULT_PORT = 8080
+
+# See what types of multiprocess server are available
+# Note: the order is important. Preferred multiprocess type
+# is the last element of this list.
+# "debug" means "none" + no tracker/template cache
+MULTIPROCESS_TYPES = ["debug", "none"]
+try:
+ import thread
+except ImportError:
+ pass
+else:
+ MULTIPROCESS_TYPES.append("thread")
+if hasattr(os, 'fork'):
+ 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, *args):
+ """ SSL.Connection can return WantRead """
+ while True:
+ try:
+ return self.__fileobj.readline(*args)
+ except SSL.WantReadError:
+ sleep (.1)
+
+ def read(self, *args):
+ """ SSL.Connection can return WantRead """
+ while True:
+ try:
+ return self.__fileobj.read(*args)
+ except SSL.WantReadError:
+ sleep (.1)
+
+ 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 = TRACKER_HOMES
- ROUNDUP_USER = ROUNDUP_USER
+ TRACKER_HOMES = {}
+ TRACKERS = None
+ LOG_IPADDRESS = 1
+ DEBUG_MODE = False
+ CONFIG = None
+
+ def get_tracker(self, name):
+ """Return a tracker instance for given tracker name"""
+ # Note: try/except KeyError works faster that has_key() check
+ # if the key is usually found in the dictionary
+ #
+ # Return cached tracker instance if we have a tracker cache
+ if self.TRACKERS:
+ try:
+ return self.TRACKERS[name]
+ except KeyError:
+ pass
+ # No cached tracker. Look for home path.
+ try:
+ tracker_home = self.TRACKER_HOMES[name]
+ except KeyError:
+ raise client.NotFound
+ # open the instance
+ tracker = roundup.instance.open(tracker_home)
+ # and cache it if we have a tracker cache
+ if self.TRACKERS:
+ self.TRACKERS[name] = tracker
+ return tracker
def run_cgi(self):
""" Execute the CGI command. Wrap an innner call in an error
self.inner_run_cgi()
except client.NotFound:
self.send_error(404, self.path)
- except client.Unauthorised:
- self.send_error(403, self.path)
+ except client.Unauthorised, message:
+ self.send_error(403, '%s (%s)'%(self.path, message))
except:
- # it'd be nice to be able to detect if these are going to have
- # any effect...
- self.send_response(400)
- self.send_header('Content-Type', 'text/html')
- self.end_headers()
- try:
- reload(cgitb)
- self.wfile.write(cgitb.breaker())
- self.wfile.write(cgitb.html())
- except:
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- self.wfile.write("<pre>")
- self.wfile.write(cgi.escape(s.getvalue()))
- self.wfile.write("</pre>\n")
+ exc, val, tb = sys.exc_info()
+ if hasattr(socket, 'timeout') and isinstance(val, socket.timeout):
+ self.log_error('timeout')
+ else:
+ # it'd be nice to be able to detect if these are going to have
+ # any effect...
+ self.send_response(400)
+ self.send_header('Content-Type', 'text/html')
+ self.end_headers()
+ if self.DEBUG_MODE:
+ try:
+ reload(cgitb)
+ self.wfile.write(cgitb.breaker())
+ self.wfile.write(cgitb.html())
+ except:
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ self.wfile.write("<pre>")
+ self.wfile.write(cgi.escape(s.getvalue()))
+ self.wfile.write("</pre>\n")
+ else:
+ # user feedback
+ self.wfile.write(cgitb.breaker())
+ ts = time.ctime()
+ self.wfile.write('''<p>%s: An error occurred. Please check
+ the server log for more infomation.</p>'''%ts)
+ # out to the logfile
+ print 'EXCEPTION AT', ts
+ traceback.print_exc()
sys.stdin = save_stdin
- do_GET = do_POST = run_cgi
+ do_GET = do_POST = do_HEAD = run_cgi
def index(self):
''' Print up an index of the available trackers
'''
- self.send_response(200)
+ keys = self.TRACKER_HOMES.keys()
+ if len(keys) == 1:
+ self.send_response(302)
+ self.send_header('Location', urllib.quote(keys[0]) + '/index')
+ self.end_headers()
+ else:
+ self.send_response(200)
+
self.send_header('Content-Type', 'text/html')
self.end_headers()
w = self.wfile.write
- w(_('<html><head><title>Roundup trackers index</title></head>\n'))
- w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
- keys = self.TRACKER_HOMES.keys()
- keys.sort()
- for tracker in keys:
- w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
- 'tracker_url': urllib.quote(tracker),
- 'tracker_name': cgi.escape(tracker)})
- w(_('</ol></body></html>'))
+
+ if self.CONFIG and self.CONFIG['TEMPLATE']:
+ template = open(self.CONFIG['TEMPLATE']).read()
+ pt = PageTemplate()
+ pt.write(template)
+ extra = { 'trackers': self.TRACKERS,
+ 'nothing' : None,
+ 'true' : 1,
+ 'false' : 0,
+ }
+ w(pt.pt_render(extra_context=extra))
+ else:
+ w(_('<html><head><title>Roundup trackers index</title></head>\n'
+ '<body><h1>Roundup trackers index</h1><ol>\n'))
+ keys.sort()
+ for tracker in keys:
+ w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
+ 'tracker_url': urllib.quote(tracker),
+ 'tracker_name': cgi.escape(tracker)})
+ w('</ol></body></html>')
def inner_run_cgi(self):
''' This is the inner part of the CGI handling
'''
rest = self.path
+ # file-like object for the favicon.ico file information
+ favicon_fileobj = None
+
if rest == '/favicon.ico':
- raise client.NotFound
-# self.send_response(200)
-# self.send_header('Content-Type', 'image/x-ico')
-# self.end_headers()
-# self.wfile.write(favicon)
-# return
+ # check to see if a custom favicon was specified, and set
+ # favicon_fileobj to the input file
+ if self.CONFIG is not None:
+ favicon_filepath = os.path.abspath(self.CONFIG['FAVICON'])
+
+ if os.access(favicon_filepath, os.R_OK):
+ favicon_fileobj = open(favicon_filepath, 'rb')
+
+
+ if favicon_fileobj is None:
+ favicon_fileobj = StringIO.StringIO(favico)
+
+ self.send_response(200)
+ self.send_header('Content-Type', 'image/x-icon')
+ self.end_headers()
+
+ # this bufsize is completely arbitrary, I picked 4K because it sounded good.
+ # if someone knows of a better buffer size, feel free to plug it in.
+ bufsize = 4 * 1024
+ Processing = True
+ while Processing:
+ data = favicon_fileobj.read(bufsize)
+ if len(data) > 0:
+ self.wfile.write(data)
+ else:
+ Processing = False
+
+ favicon_fileobj.close()
+
+ return
i = rest.rfind('?')
if i >= 0:
# no tracker - spit out the index
if rest == '/':
- return self.index()
+ self.index()
+ return
# figure the tracker
l_path = rest.split('/')
- tracker_name = urllib.unquote(l_path[1])
+ tracker_name = urllib.unquote(l_path[1]).lower()
# handle missing trailing '/'
if len(l_path) == 2:
self.wfile.write('Moved Permanently')
return
- if self.TRACKER_HOMES.has_key(tracker_name):
- tracker_home = self.TRACKER_HOMES[tracker_name]
- tracker = roundup.instance.open(tracker_home)
- else:
- raise client.NotFound
-
# figure out what the rest of the path is
if len(l_path) > 2:
rest = '/'.join(l_path[2:])
env['PATH_INFO'] = urllib.unquote(rest)
if query:
env['QUERY_STRING'] = query
- host = self.address_string()
if self.headers.typeheader is None:
env['CONTENT_TYPE'] = self.headers.type
else:
env['SERVER_NAME'] = self.server.server_name
env['SERVER_PORT'] = str(self.server.server_port)
env['HTTP_HOST'] = self.headers['host']
+ if os.environ.has_key('CGI_SHOW_TIMING'):
+ env['CGI_SHOW_TIMING'] = os.environ['CGI_SHOW_TIMING']
+ env['HTTP_ACCEPT_LANGUAGE'] = self.headers.get('accept-language')
- decoded_query = query.replace('+', ' ')
-
- # do the roundup thang
- c = tracker.Client(tracker, self, env)
- c.main()
+ # do the roundup thing
+ tracker = self.get_tracker(tracker_name)
+ tracker.Client(tracker, self, env).main()
- LOG_IPADDRESS = 1
def address_string(self):
if self.LOG_IPADDRESS:
return self.client_address[0]
host, port = self.client_address
return socket.getfqdn(host)
+ def log_message(self, format, *args):
+ ''' Try to *safely* log to stderr.
+ '''
+ try:
+ BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
+ format, *args)
+ except IOError:
+ # stderr is no longer viable
+ pass
+
+ def start_response(self, headers, response):
+ self.send_response(response)
+ for key, value in headers:
+ self.send_header(key, value)
+ self.end_headers()
+
+def error():
+ exc_type, exc_value = sys.exc_info()[:2]
+ return _('Error: %s: %s' % (exc_type, exc_value))
+
+def setgid(group):
+ if group is None:
+ return
+ if not hasattr(os, 'setgid'):
+ return
+
+ # if root, setgid to the running user
+ if os.getuid():
+ print _('WARNING: ignoring "-g" argument, not root')
+ return
+
+ try:
+ import grp
+ except ImportError:
+ raise ValueError, _("Can't change groups - no grp module")
+ try:
+ try:
+ gid = int(group)
+ except ValueError:
+ gid = grp.getgrnam(group)[2]
+ else:
+ grp.getgrgid(gid)
+ except KeyError:
+ raise ValueError,_("Group %(group)s doesn't exist")%locals()
+ os.setgid(gid)
+
+def setuid(user):
+ if not hasattr(os, 'getuid'):
+ return
+
+ # People can remove this check if they're really determined
+ if user is None:
+ if os.getuid():
+ return
+ raise ValueError, _("Can't run as root!")
+
+ if os.getuid():
+ print _('WARNING: ignoring "-u" argument, not root')
+ return
+
+ try:
+ import pwd
+ except ImportError:
+ raise ValueError, _("Can't change users - no pwd module")
+ try:
+ try:
+ uid = int(user)
+ except ValueError:
+ uid = pwd.getpwnam(user)[2]
+ else:
+ pwd.getpwuid(uid)
+ except KeyError:
+ raise ValueError, _("User %(user)s doesn't exist")%locals()
+ os.setuid(uid)
+
+class TrackerHomeOption(configuration.FilePathOption):
+
+ # Tracker homes do not need any description strings
+ def format(self):
+ return "%(name)s = %(value)s\n" % {
+ "name": self.setting,
+ "value": self.value2str(self._value),
+ }
+
+class ServerConfig(configuration.Config):
+
+ SETTINGS = (
+ ("main", (
+ (configuration.Option, "host", "",
+ "Host name of the Roundup web server instance.\n"
+ "If empty, listen on all network interfaces."),
+ (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
+ "Port to listen on."),
+ (configuration.NullableFilePathOption, "favicon", "favicon.ico",
+ "Path to favicon.ico image file."
+ " If unset, built-in favicon.ico is used."),
+ (configuration.NullableOption, "user", "",
+ "User ID as which the server will answer requests.\n"
+ "In order to use this option, "
+ "the server must be run initially as root.\n"
+ "Availability: Unix."),
+ (configuration.NullableOption, "group", "",
+ "Group ID as which the server will answer requests.\n"
+ "In order to use this option, "
+ "the server must be run initially as root.\n"
+ "Availability: Unix."),
+ (configuration.BooleanOption, "nodaemon", "no",
+ "don't fork (this overrides the pidfile mechanism)'"),
+ (configuration.BooleanOption, "log_hostnames", "no",
+ "Log client machine names instead of IP addresses "
+ "(much slower)"),
+ (configuration.NullableFilePathOption, "pidfile", "",
+ "File to which the server records "
+ "the process id of the daemon.\n"
+ "If this option is not set, "
+ "the server will run in foreground\n"),
+ (configuration.NullableFilePathOption, "logfile", "",
+ "Log file path. If unset, log to stderr."),
+ (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
+ "Set processing of each request in separate subprocess.\n"
+ "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"
+ "Option name identifies the tracker and will appear in the URL.\n"
+ "Option value is tracker home directory path.\n"
+ "The path may be either absolute or relative\n"
+ "to the directory containig this config file."),
+ )
+
+ # options recognized by config
+ OPTIONS = {
+ "host": "n:",
+ "port": "p:",
+ "group": "g:",
+ "user": "u:",
+ "logfile": "l:",
+ "pidfile": "d:",
+ "nodaemon": "D",
+ "log_hostnames": "N",
+ "multiprocess": "t:",
+ "template": "i:",
+ "ssl": "s",
+ "pem": "e:",
+ }
+
+ def __init__(self, config_file=None):
+ configuration.Config.__init__(self, config_file, self.SETTINGS)
+ self.sections.append("trackers")
+
+ def _adjust_options(self, config):
+ """Add options for tracker homes"""
+ # return early if there are no tracker definitions.
+ # trackers must be specified on the command line.
+ if not config.has_section("trackers"):
+ return
+ # config defaults appear in all sections.
+ # filter them out.
+ defaults = config.defaults().keys()
+ for name in config.options("trackers"):
+ if name not in defaults:
+ self.add_option(TrackerHomeOption(self, "trackers", name))
+
+ def getopt(self, args, short_options="", long_options=(),
+ config_load_options=("C", "config"), **options
+ ):
+ options.update(self.OPTIONS)
+ return configuration.Config.getopt(self, args,
+ short_options, long_options, config_load_options, **options)
+
+ def _get_name(self):
+ return "Roundup server"
+
+ def trackers(self):
+ """Return tracker definitions as a list of (name, home) pairs"""
+ trackers = []
+ for option in self._get_section_options("trackers"):
+ trackers.append((option, os.path.abspath(
+ self["TRACKERS_" + option.upper()])))
+ return trackers
+
+ def set_logging(self):
+ """Initialise logging to the configured file, if any."""
+ # appending, unbuffered
+ sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0)
+
+ def get_server(self):
+ """Return HTTP server object to run"""
+ # we don't want the cgi module interpreting the command-line args ;)
+ sys.argv = sys.argv[:1]
+
+ # preload all trackers unless we are in "debug" mode
+ tracker_homes = self.trackers()
+ if self["MULTIPROCESS"] == "debug":
+ trackers = None
+ else:
+ trackers = dict([(name, roundup.instance.open(home, optimize=1))
+ for (name, home) in tracker_homes])
+
+ # build customized request handler class
+ class RequestHandler(RoundupRequestHandler):
+ LOG_IPADDRESS = not self["LOG_HOSTNAMES"]
+ TRACKER_HOMES = dict(tracker_homes)
+ TRACKERS = trackers
+ DEBUG_MODE = self["MULTIPROCESS"] == "debug"
+ CONFIG = self
+
+ if self["SSL"]:
+ base_server = SecureHTTPServer
+ else:
+ # time out after a minute if we can
+ # This sets the socket to non-blocking. SSL needs a blocking
+ # socket, so we do this only for non-SSL connections.
+ if hasattr(socket, 'setdefaulttimeout'):
+ socket.setdefaulttimeout(60)
+ 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 = base_server
+ elif self["MULTIPROCESS"] == "fork":
+ class ForkingServer(SocketServer.ForkingMixIn,
+ base_server):
+ pass
+ server_class = ForkingServer
+ elif self["MULTIPROCESS"] == "thread":
+ class ThreadingServer(SocketServer.ThreadingMixIn,
+ base_server):
+ pass
+ server_class = ThreadingServer
+ else:
+ server_class = base_server
+
+ # obtain server before changing user id - allows to
+ # use port < 1024 if started as root
+ try:
+ 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, \
+ _("Unable to bind to port %s, port already in use.") \
+ % self["PORT"]
+ raise
+ # change user and/or group
+ setgid(self["GROUP"])
+ setuid(self["USER"])
+ # return the server
+ return httpd
+
try:
import win32serviceutil
except:
RoundupService = None
else:
+
# allow the win32
import win32service
- import win32event
- from win32event import *
- from win32file import *
- SvcShutdown = "ServiceShutdown"
+ class SvcShutdown(Exception):
+ pass
- class RoundupService(win32serviceutil.ServiceFramework,
- BaseHTTPServer.HTTPServer):
- ''' A Roundup standalone server for Win32 by Ewout Prangsma
- '''
- _svc_name_ = "Roundup Bug Tracker"
+ class RoundupService(win32serviceutil.ServiceFramework):
+
+ _svc_name_ = "roundup"
_svc_display_name_ = "Roundup Bug Tracker"
- address = ('', 8888)
- def __init__(self, args):
- win32serviceutil.ServiceFramework.__init__(self, args)
- BaseHTTPServer.HTTPServer.__init__(self, self.address,
- RoundupRequestHandler)
-
- # Create the necessary NT Event synchronization objects...
- # hevSvcStop is signaled when the SCM sends us a notification
- # to shutdown the service.
- self.hevSvcStop = win32event.CreateEvent(None, 0, 0, None)
-
- # hevConn is signaled when we have a new incomming connection.
- self.hevConn = win32event.CreateEvent(None, 0, 0, None)
-
- # Hang onto this module for other people to use for logging
- # purposes.
- import servicemanager
- self.servicemanager = servicemanager
- def SvcStop(self):
- # Before we do anything, tell the SCM we are starting the
- # stop process.
- self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
- win32event.SetEvent(self.hevSvcStop)
+ running = 0
+ server = None
def SvcDoRun(self):
- try:
- self.serve_forever()
- except SvcShutdown:
- pass
-
- def get_request(self):
- # Call WSAEventSelect to enable self.socket to be waited on.
- WSAEventSelect(self.socket, self.hevConn, FD_ACCEPT)
- while 1:
- try:
- rv = self.socket.accept()
- except socket.error, why:
- if why[0] != WSAEWOULDBLOCK:
- raise
- # Use WaitForMultipleObjects instead of select() because
- # on NT select() is only good for sockets, and not general
- # NT synchronization objects.
- rc = WaitForMultipleObjects((self.hevSvcStop, self.hevConn),
- 0, INFINITE)
- if rc == WAIT_OBJECT_0:
- # self.hevSvcStop was signaled, this means:
- # Stop the service!
- # So we throw the shutdown exception, which gets
- # caught by self.SvcDoRun
- raise SvcShutdown
- # Otherwise, rc == WAIT_OBJECT_0 + 1 which means
- # self.hevConn was signaled, which means when we call
- # self.socket.accept(), we'll have our incoming connection
- # socket!
- # Loop back to the top, and let that accept do its thing...
- else:
- # yay! we have a connection
- # However... the new socket is non-blocking, we need to
- # set it back into blocking mode. (The socket that accept()
- # returns has the same properties as the listening sockets,
- # this includes any properties set by WSAAsyncSelect, or
- # WSAEventSelect, and whether its a blocking socket or not.)
- #
- # So if you yank the following line, the setblocking() call
- # will be useless. The socket will still be in non-blocking
- # mode.
- WSAEventSelect(rv[0], self.hevConn, 0)
- rv[0].setblocking(1)
- break
- return rv
+ import servicemanager
+ self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
+ config = ServerConfig()
+ (optlist, args) = config.getopt(sys.argv[1:])
+ if not config["LOGFILE"]:
+ servicemanager.LogMsg(servicemanager.EVENTLOG_ERROR_TYPE,
+ servicemanager.PYS_SERVICE_STOPPED,
+ (self._svc_display_name_, "\r\nMissing logfile option"))
+ self.ReportServiceStatus(win32service.SERVICE_STOPPED)
+ return
+ config.set_logging()
+ self.server = config.get_server()
+ self.running = 1
+ self.ReportServiceStatus(win32service.SERVICE_RUNNING)
+ servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
+ servicemanager.PYS_SERVICE_STARTED, (self._svc_display_name_,
+ " at %s:%s" % (config["HOST"], config["PORT"])))
+ while self.running:
+ self.server.handle_request()
+ servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
+ servicemanager.PYS_SERVICE_STOPPED,
+ (self._svc_display_name_, ""))
+ self.ReportServiceStatus(win32service.SERVICE_STOPPED)
+ def SvcStop(self):
+ self.running = 0
+ # make dummy connection to self to terminate blocking accept()
+ addr = self.server.socket.getsockname()
+ if addr[0] == "0.0.0.0":
+ addr = ("127.0.0.1", addr[1])
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect(addr)
+ sock.close()
+ self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
def usage(message=''):
+ if RoundupService:
+ os_part = \
+""''' -c <Command> Windows Service options.
+ If you want to run the server as a Windows Service, you
+ must use configuration file to specify tracker homes.
+ Logfile option is required to run Roundup Tracker service.
+ Typing "roundup-server -c help" shows Windows Services
+ specifics.'''
+ else:
+ os_part = ""''' -u <UID> runs the Roundup web server as this UID
+ -g <GID> runs the Roundup web server as this GID
+ -d <PIDfile> run the server in the background and write the server's PID
+ to the file indicated by PIDfile. The -l option *must* be
+ specified if -d is used.'''
if message:
- message = _('Error: %(error)s\n\n')%{'error': message}
- print _('''%(message)sUsage:
-roundup-server [options] [name=tracker home]*
-
-options:
- -n: sets the host name
- -p: sets the port to listen on
- -l: sets a filename to log to (instead of stdout)
- -d: sets a filename to write server PID to. This option causes the server
- to run in the background. Note: on Windows the PID argument is needed,
- but ignored. The -l option *must* be specified if this option is.
- -N: log client machine names in access log instead of IP addresses (much
- slower)
-
-name=tracker home:
- Sets the tracker home(s) to use. The name is how the tracker is
- identified in the URL (it's the first part of the URL path). The
- tracker home is the directory that was identified when you did
+ message += '\n'
+ print _('''%(message)sUsage: roundup-server [options] [name=tracker home]*
+
+Options:
+ -v print the Roundup version number and exit
+ -h print this text and exit
+ -S create or update configuration file and exit
+ -C <fname> use configuration file <fname>
+ -n <name> set the host name of the Roundup web server instance
+ -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 <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
+
+Long options:
+ --version print the Roundup version number and exit
+ --help print this text and exit
+ --save-config create or update configuration file and exit
+ --config <fname> use configuration file <fname>
+ All settings of the [main] section of the configuration file
+ also may be specified in form --<name>=<value>
+
+Examples:
+
+ roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\
+ -n localhost -p 8917 -l /var/log/roundup.log \\
+ support=/var/spool/roundup-trackers/support
+
+ roundup-server -C /opt/roundup/etc/roundup-server.ini
+
+ roundup-server support=/var/spool/roundup-trackers/support
+
+ roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\
+ support=/var/spool/roundup-trackers/support
+
+Configuration file format:
+ Roundup Server configuration file has common .ini file format.
+ Configuration file created with 'roundup-server -S' contains
+ detailed explanations for each option. Please see that file
+ for option descriptions.
+
+How to use "name=tracker home":
+ These arguments set the tracker home(s) to use. The name is how the
+ tracker is identified in the URL (it's the first part of the URL path).
+ The tracker home is the directory that was identified when you did
"roundup-admin init". You may specify any number of these name=home
- pairs on the command-line. For convenience, you may edit the
- TRACKER_HOMES variable in the roundup-server file instead.
- Make sure the name part doesn't include any url-unsafe characters like
- spaces, as these confuse the cookie handling in browsers like IE.
-''')%locals()
- sys.exit(0)
+ pairs on the command-line. Make sure the name part doesn't include
+ any url-unsafe characters like spaces, as these confuse IE.
+''') % {
+ "message": message,
+ "os_part": os_part,
+ "port": DEFAULT_PORT,
+ "mp_def": DEFAULT_MULTIPROCESS,
+ "mp_types": ", ".join(MULTIPROCESS_TYPES),
+}
+
+
+def writepidfile(pidfile):
+ ''' Write a pidfile (only). Do not daemonize. '''
+ pid = os.getpid()
+ if pid:
+ pidfile = open(pidfile, 'w')
+ pidfile.write(str(pid))
+ pidfile.close()
def daemonize(pidfile):
''' Turn this process into a daemon.
pidfile = open(pidfile, 'w')
pidfile.write(str(pid))
pidfile.close()
- os._exit(0)
+ os._exit(0)
- os.chdir("/")
- os.umask(0)
+ os.chdir("/")
- # close off sys.std(in|out|err), redirect to devnull so the file
+ # close off std(in|out|err), redirect to devnull so the file
# descriptors can't be used again
devnull = os.open('/dev/null', 0)
os.dup2(devnull, 0)
os.dup2(devnull, 1)
os.dup2(devnull, 2)
-def abspath(path):
- ''' Make the given path an absolute path.
-
- Code from Zope-Coders posting of 2002-10-06 by GvR.
- '''
- if not os.path.isabs(path):
- path = os.path.join(os.getcwd(), path)
- return os.path.normpath(path)
-
-def run():
+undefined = []
+def run(port=undefined, success_message=None):
''' Script entry point - handle args and figure out what to to.
'''
- # time out after a minute if we can
- import socket
- if hasattr(socket, 'setdefaulttimeout'):
- socket.setdefaulttimeout(60)
-
- hostname = ''
- port = 8080
- pidfile = None
- logfile = None
+ config = ServerConfig()
+ # additional options
+ short_options = "hvS"
+ if RoundupService:
+ short_options += 'c'
try:
- # handle the command-line args
- try:
- optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:hN')
- except getopt.GetoptError, e:
- usage(str(e))
-
- user = ROUNDUP_USER
+ (optlist, args) = config.getopt(sys.argv[1:],
+ short_options, ("help", "version", "save-config",))
+ except (getopt.GetoptError, configuration.ConfigurationError), e:
+ usage(str(e))
+ return
+
+ # if running in windows service mode, don't do any other stuff
+ if ("-c", "") in optlist:
+ # acquire command line options recognized by service
+ short_options = "cC:"
+ long_options = ["config"]
+ for (long_name, short_name) in config.OPTIONS.items():
+ short_options += short_name
+ long_name = long_name.lower().replace("_", "-")
+ if short_name[-1] == ":":
+ long_name += "="
+ long_options.append(long_name)
+ optlist = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
+ svc_args = []
for (opt, arg) in optlist:
- if opt == '-n': hostname = arg
- elif opt == '-p': port = int(arg)
- elif opt == '-u': user = arg
- elif opt == '-d': pidfile = abspath(arg)
- elif opt == '-l': logfile = abspath(arg)
- elif opt == '-h': usage()
- elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
-
- if pidfile and not logfile:
- raise ValueError, _("logfile *must* be specified if pidfile is")
-
- if hasattr(os, 'getuid'):
- # if root, setuid to the running user
- if not os.getuid() and user is not None:
- try:
- import pwd
- except ImportError:
- raise ValueError, _("Can't change users - no pwd module")
- try:
- uid = pwd.getpwnam(user)[2]
- except KeyError:
- raise ValueError, _("User %(user)s doesn't exist")%locals()
- os.setuid(uid)
- elif os.getuid() and user is not None:
- print _('WARNING: ignoring "-u" argument, not root')
-
- # People can remove this check if they're really determined
- if not os.getuid() and user is None:
- raise ValueError, _("Can't run as root!")
-
- # handle tracker specs
- if args:
- d = {}
- for arg in args:
- try:
- name, home = arg.split('=')
- except ValueError:
- raise ValueError, _("Instances must be name=home")
- d[name] = home
- RoundupRequestHandler.TRACKER_HOMES = d
- except SystemExit:
- raise
- except:
- exc_type, exc_value = sys.exc_info()[:2]
- usage('%s: %s'%(exc_type, exc_value))
-
- # we don't want the cgi module interpreting the command-line args ;)
- sys.argv = sys.argv[:1]
- address = (hostname, port)
-
- # fork?
- if pidfile:
- if RoundupService:
- # don't do any other stuff
- RoundupService.address = address
- return win32serviceutil.HandleCommandLine(RoundupService)
- elif not hasattr(os, 'fork'):
- print "Sorry, you can't run the server as a daemon on this" \
- 'Operating System'
+ if opt in ("-C", "-l"):
+ # make sure file name is absolute
+ svc_args.extend((opt, os.path.abspath(arg)))
+ elif opt in ("--config", "--logfile"):
+ # ditto, for long options
+ svc_args.append("=".join(opt, os.path.abspath(arg)))
+ elif opt != "-c":
+ svc_args.extend(opt)
+ RoundupService._exe_args_ = " ".join(svc_args)
+ # pass the control to serviceutil
+ win32serviceutil.HandleCommandLine(RoundupService,
+ argv=sys.argv[:1] + args)
+ return
+
+ # add tracker names from command line.
+ # this is done early to let '--save-config' handle the trackers.
+ if args:
+ for arg in args:
+ try:
+ name, home = arg.split('=')
+ except ValueError:
+ raise ValueError, _("Instances must be name=home")
+ config.add_option(TrackerHomeOption(config, "trackers", name))
+ config["TRACKERS_" + name.upper()] = home
+
+ # handle remaining options
+ if optlist:
+ for (opt, arg) in optlist:
+ if opt in ("-h", "--help"):
+ usage()
+ elif opt in ("-v", "--version"):
+ print '%s (python %s)' % (roundup_version,
+ sys.version.split()[0])
+ elif opt in ("-S", "--save-config"):
+ config.save()
+ print _("Configuration saved to %s") % config.filepath
+ # any of the above options prevent server from running
+ return
+
+ # port number in function arguments overrides config and command line
+ if port is not undefined:
+ config.PORT = port
+
+ if config["LOGFILE"]:
+ config["LOGFILE"] = os.path.abspath(config["LOGFILE"])
+ # switch logging from stderr/stdout to logfile
+ config.set_logging()
+ if config["PIDFILE"]:
+ config["PIDFILE"] = os.path.abspath(config["PIDFILE"])
+
+ # fork the server from our parent if a pidfile is specified
+ if config["PIDFILE"]:
+ if not hasattr(os, 'fork'):
+ print _("Sorry, you can't run the server as a daemon"
+ " on this Operating System")
sys.exit(0)
else:
- daemonize(pidfile)
+ if config['NODAEMON']:
+ writepidfile(config["PIDFILE"])
+ else:
+ daemonize(config["PIDFILE"])
- # redirect stdout/stderr to our logfile
- if logfile:
- # appending, unbuffered
- sys.stdout = sys.stderr = open(logfile, 'a', 0)
+ # create the server
+ httpd = config.get_server()
+
+ if success_message:
+ print success_message
+ else:
+ print _('Roundup server started on %(HOST)s:%(PORT)s') \
+ % config
- httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
- print _('Roundup server started on %(address)s')%locals()
try:
httpd.serve_forever()
except KeyboardInterrupt:
if __name__ == '__main__':
run()
-# vim: set filetype=python ts=4 sw=4 et si
+# vim: sts=4 sw=4 et si