X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fscripts%2Froundup_server.py;h=c14cdf6abf597b70ae1e60422ce2b9333164cac8;hb=ebceedf66795d79ee76ecc869abdcd8e65ac3a81;hp=72d7d380b0fac9c9220af2cdd59c922806d186e6;hpb=21b3acdf5b78d378f1f9044815f69f33bd2f168f;p=roundup.git diff --git a/roundup/scripts/roundup_server.py b/roundup/scripts/roundup_server.py index 72d7d38..c14cdf6 100644 --- a/roundup/scripts/roundup_server.py +++ b/roundup/scripts/roundup_server.py @@ -14,22 +14,30 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -""" HTTP Server that serves roundup. -$Id: roundup_server.py,v 1.12 2002-09-23 06:48:35 richard Exp $ +"""Command-line script that runs a server over roundup.cgi.client. + +$Id: roundup_server.py,v 1.43 2004-04-05 23:43:04 richard Exp $ """ +__docformat__ = 'restructuredtext' # python version check from roundup import version_check +from roundup import __version__ as roundup_version import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp -import BaseHTTPServer +import BaseHTTPServer, socket, errno # Roundup modules of use here from roundup.cgi import cgitb, client import roundup.instance from roundup.i18n import _ +try: + import signal +except: + signal = None + # ## Configuration # @@ -40,25 +48,38 @@ from roundup.i18n import _ # 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 = { - 'bar': '/tmp/bar', +# 'example': '/path/to/example', } ROUNDUP_USER = None +ROUNDUP_GROUP = None +ROUNDUP_LOG_IP = 1 +HOSTNAME = '' +PORT = 8080 +PIDFILE = None +LOGFILE = None -# Where to log debugging information to. Use an instance of DevNull if you -# don't want to log anywhere. -# TODO: actually use this stuff -#class DevNull: -# def write(self, info): -# pass -#LOG = open('/var/log/roundup.cgi.log', 'a') -#LOG = DevNull() - # ## end configuration # +# "default" favicon.ico +# generate by using "icotool" and tools/base64 +import zlib, base64 +favico = zlib.decompress(base64.decodestring(''' +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())) + class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): TRACKER_HOMES = TRACKER_HOMES ROUNDUP_USER = ROUNDUP_USER @@ -76,59 +97,88 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): except client.Unauthorised: self.send_error(403, self.path) 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: - self.wfile.write("
")
+            exc, val, tb = sys.exc_info()
+            if hasattr(socket, 'timeout') and exc == socket.timeout:
                 s = StringIO.StringIO()
                 traceback.print_exc(None, s)
-                self.wfile.write(cgi.escape(s.getvalue()))
-                self.wfile.write("
\n") + self.log_message(str(s.getvalue())) + 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() + 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("
")
+                    self.wfile.write(cgi.escape(s.getvalue()))
+                    self.wfile.write("
\n") sys.stdin = save_stdin - do_GET = do_POST = do_HEAD = send_head = run_cgi + do_GET = do_POST = run_cgi def index(self): - ''' Print up an index of the available instances + ''' Print up an index of the available trackers ''' self.send_response(200) self.send_header('Content-Type', 'text/html') self.end_headers() w = self.wfile.write - w(_('Roundup instances index\n')) - w(_('

Roundup instances index

    \n')) - for instance in self.TRACKER_HOMES.keys(): - w(_('
  1. %(instance_name)s\n')%{ - 'instance_url': urllib.quote(instance), - 'instance_name': cgi.escape(instance)}) + w(_('Roundup trackers index\n')) + w(_('

    Roundup trackers index

      \n')) + keys = self.TRACKER_HOMES.keys() + keys.sort() + for tracker in keys: + w(_('
    1. %(tracker_name)s\n')%{ + 'tracker_url': urllib.quote(tracker), + 'tracker_name': cgi.escape(tracker)}) w(_('
    ')) def inner_run_cgi(self): ''' This is the inner part of the CGI handling ''' - rest = self.path + + if rest == '/favicon.ico': + self.send_response(200) + self.send_header('Content-Type', 'image/x-icon') + self.end_headers() + self.wfile.write(favico) + return + i = rest.rfind('?') if i >= 0: rest, query = rest[:i], rest[i+1:] else: query = '' - # figure the instance + # no tracker - spit out the index if rest == '/': return self.index() + + # figure the tracker l_path = rest.split('/') - instance_name = urllib.unquote(l_path[1]) - if self.TRACKER_HOMES.has_key(instance_name): - instance_home = self.TRACKER_HOMES[instance_name] - instance = roundup.instance.open(instance_home) + tracker_name = urllib.unquote(l_path[1]) + + # handle missing trailing '/' + if len(l_path) == 2: + self.send_response(301) + # redirect - XXX https?? + protocol = 'http' + url = '%s://%s%s/'%(protocol, self.headers['host'], self.path) + self.send_header('Location', url) + self.end_headers() + 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 @@ -140,7 +190,7 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # Set up the CGI environment env = {} - env['TRACKER_NAME'] = instance_name + env['TRACKER_NAME'] = tracker_name env['REQUEST_METHOD'] = self.command env['PATH_INFO'] = urllib.unquote(rest) if query: @@ -156,6 +206,7 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): co = filter(None, self.headers.getheaders('cookie')) if co: env['HTTP_COOKIE'] = ', '.join(co) + env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization') env['SCRIPT_NAME'] = '' env['SERVER_NAME'] = self.server.server_name env['SERVER_PORT'] = str(self.server.server_port) @@ -164,24 +215,159 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): decoded_query = query.replace('+', ' ') # do the roundup thang - c = instance.Client(instance, self, env) + c = tracker.Client(tracker, self, env) c.main() -def usage(message=''): - if message: - message = _('Error: %(error)s\n\n')%{'error': message} - print _('''%(message)sUsage: -roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=instance home]* + LOG_IPADDRESS = ROUNDUP_LOG_IP + def address_string(self): + if self.LOG_IPADDRESS: + return self.client_address[0] + else: + host, port = self.client_address + return socket.getfqdn(host) - -n: sets the host name - -p: sets the port to listen on - -l: sets a filename to log to (instead of stdout) - -d: daemonize, and write the server's PID to the nominated file + 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 error(): + exc_type, exc_value = sys.exc_info()[:2] + return _('Error: %s: %s' % (exc_type, exc_value)) + +try: + import win32serviceutil +except: + RoundupService = None +else: + # allow the win32 + import win32service + import win32event + from win32event import * + from win32file import * + + SvcShutdown = "ServiceShutdown" + + class RoundupService(win32serviceutil.ServiceFramework, + BaseHTTPServer.HTTPServer): + ''' A Roundup standalone server for Win32 by Ewout Prangsma + ''' + _svc_name_ = "Roundup Bug Tracker" + _svc_display_name_ = "Roundup Bug Tracker" + address = (HOSTNAME, PORT) + def __init__(self, args): + # redirect stdout/stderr to our logfile + if LOGFILE: + # appending, unbuffered + sys.stdout = sys.stderr = open(LOGFILE, 'a', 0) + 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) + + 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 - name=instance home - Sets the instance home(s) to use. The name is how the instance is +def usage(message=''): + if RoundupService: + win = ''' -c: Windows Service options. If you want to run the server as a Windows + Service, you must configure the rest of the options by changing the + constants of this program. You will at least configure one tracker + in the TRACKER_HOMES variable. This option is mutually exclusive + from the rest. Typing "roundup-server -c help" shows Windows + Services specifics.''' + else: + win = '' + port=PORT + print _('''%(message)s +Usage: +roundup-server [options] [name=tracker home]* + +options: + -v: print version and exit + -n: sets the host name + -p: sets the port to listen on (default: %(port)s) + -u: sets the uid to this user after listening on the port + -g: sets the gid to this group after listening on the port + -l: sets a filename to log to (instead of stderr / stdout) + -d: run the server in the background and on UN*X write the server's PID + to the nominated file. The -l option *must* be specified if this + option is. + -N: log client machine names in access log instead of IP addresses (much + slower) +%(win)s + +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 - instance home is the directory that was identified when you did + 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. @@ -213,9 +399,9 @@ def daemonize(pidfile): pidfile = open(pidfile, 'w') pidfile.write(str(pid)) pidfile.close() - os._exit(0) + os._exit(0) - os.chdir("/") + os.chdir("/") os.umask(0) # close off sys.std(in|out|err), redirect to devnull so the file @@ -225,26 +411,79 @@ def daemonize(pidfile): os.dup2(devnull, 1) os.dup2(devnull, 2) -def run(): - hostname = '' - port = 8080 - pidfile = None - logfile = None +def run(port=PORT, 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 = HOSTNAME + pidfile = PIDFILE + logfile = LOGFILE + user = ROUNDUP_USER + group = ROUNDUP_GROUP + svc_args = None + try: # handle the command-line args + options = 'n:p:u:d:l:hNv' + if RoundupService: + options += 'c' + try: - optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:') + optlist, args = getopt.getopt(sys.argv[1:], options) except getopt.GetoptError, e: usage(str(e)) user = ROUNDUP_USER + group = None for (opt, arg) in optlist: if opt == '-n': hostname = arg + elif opt == '-v': + print '%s (python %s)'%(roundup_version, sys.version.split()[0]) + return elif opt == '-p': port = int(arg) elif opt == '-u': user = arg - elif opt == '-d': pidfile = arg - elif opt == '-l': logfile = arg + elif opt == '-g': group = arg + elif opt == '-d': pidfile = os.path.abspath(arg) + elif opt == '-l': logfile = os.path.abspath(arg) elif opt == '-h': usage() + elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0 + elif opt == '-c': svc_args = [opt] + args; args = None + + if svc_args is not None and len(optlist) > 1: + raise ValueError, _("windows service option must be the only one") + + if pidfile and not logfile: + raise ValueError, _("logfile *must* be specified if pidfile is") + + # obtain server before changing user id - allows to use port < + # 1024 if started as root + address = (hostname, port) + try: + httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler) + except socket.error, e: + if e[0] == errno.EADDRINUSE: + raise socket.error, \ + _("Unable to bind to port %s, port already in use." % port) + raise + + if group is not None and hasattr(os, 'getgid'): + # if root, setgid to the running user + if not os.getgid() and user is not None: + try: + import pwd + except ImportError: + raise ValueError, _("Can't change groups - no pwd module") + try: + gid = pwd.getpwnam(user)[3] + except KeyError: + raise ValueError,_("Group %(group)s doesn't exist")%locals() + os.setgid(gid) + elif os.getgid() and user is not None: + print _('WARNING: ignoring "-g" argument, not root') if hasattr(os, 'getuid'): # if root, setuid to the running user @@ -265,37 +504,53 @@ def run(): if not os.getuid() and user is None: raise ValueError, _("Can't run as root!") - # handle instance specs + # handle tracker specs if args: d = {} for arg in args: - try: + try: name, home = arg.split('=') except ValueError: raise ValueError, _("Instances must be name=home") - d[name] = home + d[name] = os.path.abspath(home) RoundupRequestHandler.TRACKER_HOMES = d except SystemExit: raise + except ValueError: + usage(error()) except: - exc_type, exc_value = sys.exc_info()[:2] - usage('%s: %s'%(exc_type, exc_value)) + print error() + sys.exit(1) # we don't want the cgi module interpreting the command-line args ;) sys.argv = sys.argv[:1] - address = (hostname, port) - # fork? if pidfile: - daemonize(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 svc_args is not None: + # don't do any other stuff + return win32serviceutil.HandleCommandLine(RoundupService, argv=svc_args) # redirect stdout/stderr to our logfile if logfile: - sys.stdout = sys.stderr = open(logfile, 'a') + # appending, unbuffered + sys.stdout = sys.stderr = open(logfile, 'a', 0) - httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler) - print _('Roundup server started on %(address)s')%locals() - httpd.serve_forever() + if success_message: + print success_message + else: + print _('Roundup server started on %(address)s')%locals() + + try: + httpd.serve_forever() + except KeyboardInterrupt: + print 'Keyboard Interrupt: exiting' if __name__ == '__main__': run()