1 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
2 # This module is free software, and you may redistribute it and/or modify
3 # under the same terms as Python, so long as this copyright message and
4 # disclaimer are retained in their original form.
5 #
6 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
7 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
8 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
9 # POSSIBILITY OF SUCH DAMAGE.
10 #
11 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
12 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
14 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 #
18 """Command-line script that runs a server over roundup.cgi.client.
20 $Id: roundup_server.py,v 1.39 2004-02-15 21:55:10 richard Exp $
21 """
22 __docformat__ = 'restructuredtext'
24 # python version check
25 from roundup import version_check
27 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
28 import BaseHTTPServer, socket, errno
30 # Roundup modules of use here
31 from roundup.cgi import cgitb, client
32 import roundup.instance
33 from roundup.i18n import _
35 #
36 ## Configuration
37 #
39 # This indicates where the Roundup trackers live. They're given as NAME ->
40 # TRACKER_HOME, where the NAME part is used in the URL to select the
41 # appropriate reacker.
42 # Make sure the NAME part doesn't include any url-unsafe characters like
43 # spaces, as these confuse the cookie handling in browsers like IE.
44 TRACKER_HOMES = {
45 # 'example': '/path/to/example',
46 }
48 ROUNDUP_USER = None
49 ROUNDUP_GROUP = None
50 ROUNDUP_LOG_IP = 1
51 HOSTNAME = ''
52 PORT = 8080
53 PIDFILE = None
54 LOGFILE = None
57 #
58 ## end configuration
59 #
61 import zlib, base64
62 favico = zlib.decompress(base64.decodestring('''
63 eJyVUk2IQVEUfn4yaRYjibdQZiVba/ZE2djIUmHWFjaKGVmIlY2iFMVG2ViQhXqFSP6iFFJvw4uF
64 LGdWd743mpeMn+a88917Oue7955z3qEoET6FQkHx8iahKDV2A8B7XgERRf/EKMSUzyf8ypbbnnQy
65 mWBdr9eVSkVw3tJGoxGNRpvNZigUyufzWPv9Pvwcx0UiERj7/V4g73Y7j8fTarWMRmO73U4kEkKI
66 YZhardbr9eLxuOD0+/2ZTMZisYjFYpqmU6kU799uN5tNMBg8HA7ZbPY8GaTh8/mEipRKpclk0ul0
67 NpvNarUmk0mWZS/yr9frcrmc+iMOh+NWydPp1Ov1SiSSc344HL7fKKfTiSN2u12tVqOcxWJxn6/V
68 ag0GAwxkrlKp5vP5fT7ulMlk6XRar9dLpVIUXi6Xb5Hxa1wul0ajKZVKsVjM7XYXCoVOp3OVPJvN
69 AoFAtVo1m825XO7hSODOYrH4kHbxxGAwwODBGI/H6DBs5LNara7yl8slGjIcDsHpdrunU6PRCAP2
70 r3fPdUcIYeyEfLSAJ0LeAUZHCAt8Al/8/kLIEWDB5YDj0wm8fAP6fVfo
71 '''.strip()))
73 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
74 TRACKER_HOMES = TRACKER_HOMES
75 ROUNDUP_USER = ROUNDUP_USER
77 def run_cgi(self):
78 """ Execute the CGI command. Wrap an innner call in an error
79 handler so all errors can be caught.
80 """
81 save_stdin = sys.stdin
82 sys.stdin = self.rfile
83 try:
84 self.inner_run_cgi()
85 except client.NotFound:
86 self.send_error(404, self.path)
87 except client.Unauthorised:
88 self.send_error(403, self.path)
89 except:
90 exc, val, tb = sys.exc_info()
91 if hasattr(socket, 'timeout') and exc == socket.timeout:
92 s = StringIO.StringIO()
93 traceback.print_exc(None, s)
94 self.log_message(str(s.getvalue()))
95 else:
96 # it'd be nice to be able to detect if these are going to have
97 # any effect...
98 self.send_response(400)
99 self.send_header('Content-Type', 'text/html')
100 self.end_headers()
101 try:
102 reload(cgitb)
103 self.wfile.write(cgitb.breaker())
104 self.wfile.write(cgitb.html())
105 except:
106 s = StringIO.StringIO()
107 traceback.print_exc(None, s)
108 self.wfile.write("<pre>")
109 self.wfile.write(cgi.escape(s.getvalue()))
110 self.wfile.write("</pre>\n")
111 sys.stdin = save_stdin
113 do_GET = do_POST = run_cgi
115 def index(self):
116 ''' Print up an index of the available trackers
117 '''
118 self.send_response(200)
119 self.send_header('Content-Type', 'text/html')
120 self.end_headers()
121 w = self.wfile.write
122 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
123 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
124 keys = self.TRACKER_HOMES.keys()
125 keys.sort()
126 for tracker in keys:
127 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
128 'tracker_url': urllib.quote(tracker),
129 'tracker_name': cgi.escape(tracker)})
130 w(_('</ol></body></html>'))
132 def inner_run_cgi(self):
133 ''' This is the inner part of the CGI handling
134 '''
135 rest = self.path
137 if rest == '/favicon.ico':
138 raise client.NotFound
140 i = rest.rfind('?')
141 if i >= 0:
142 rest, query = rest[:i], rest[i+1:]
143 else:
144 query = ''
146 # no tracker - spit out the index
147 if rest == '/':
148 return self.index()
150 # figure the tracker
151 l_path = rest.split('/')
152 tracker_name = urllib.unquote(l_path[1])
154 # handle missing trailing '/'
155 if len(l_path) == 2:
156 self.send_response(301)
157 # redirect - XXX https??
158 protocol = 'http'
159 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
160 self.send_header('Location', url)
161 self.end_headers()
162 self.wfile.write('Moved Permanently')
163 return
165 if self.TRACKER_HOMES.has_key(tracker_name):
166 tracker_home = self.TRACKER_HOMES[tracker_name]
167 tracker = roundup.instance.open(tracker_home)
168 else:
169 raise client.NotFound
171 # figure out what the rest of the path is
172 if len(l_path) > 2:
173 rest = '/'.join(l_path[2:])
174 else:
175 rest = '/'
177 # Set up the CGI environment
178 env = {}
179 env['TRACKER_NAME'] = tracker_name
180 env['REQUEST_METHOD'] = self.command
181 env['PATH_INFO'] = urllib.unquote(rest)
182 if query:
183 env['QUERY_STRING'] = query
184 host = self.address_string()
185 if self.headers.typeheader is None:
186 env['CONTENT_TYPE'] = self.headers.type
187 else:
188 env['CONTENT_TYPE'] = self.headers.typeheader
189 length = self.headers.getheader('content-length')
190 if length:
191 env['CONTENT_LENGTH'] = length
192 co = filter(None, self.headers.getheaders('cookie'))
193 if co:
194 env['HTTP_COOKIE'] = ', '.join(co)
195 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
196 env['SCRIPT_NAME'] = ''
197 env['SERVER_NAME'] = self.server.server_name
198 env['SERVER_PORT'] = str(self.server.server_port)
199 env['HTTP_HOST'] = self.headers['host']
201 decoded_query = query.replace('+', ' ')
203 # do the roundup thang
204 c = tracker.Client(tracker, self, env)
205 c.main()
207 LOG_IPADDRESS = ROUNDUP_LOG_IP
208 def address_string(self):
209 if self.LOG_IPADDRESS:
210 return self.client_address[0]
211 else:
212 host, port = self.client_address
213 return socket.getfqdn(host)
215 def log_message(self, format, *args):
216 ''' Try to *safely* log to stderr.
217 '''
218 try:
219 BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
220 format, *args)
221 except IOError:
222 # stderr is no longer viable
223 pass
225 def error():
226 exc_type, exc_value = sys.exc_info()[:2]
227 return _('Error: %s: %s' % (exc_type, exc_value))
229 try:
230 import win32serviceutil
231 except:
232 RoundupService = None
233 else:
234 # allow the win32
235 import win32service
236 import win32event
237 from win32event import *
238 from win32file import *
240 SvcShutdown = "ServiceShutdown"
242 class RoundupService(win32serviceutil.ServiceFramework,
243 BaseHTTPServer.HTTPServer):
244 ''' A Roundup standalone server for Win32 by Ewout Prangsma
245 '''
246 _svc_name_ = "Roundup Bug Tracker"
247 _svc_display_name_ = "Roundup Bug Tracker"
248 address = (HOSTNAME, PORT)
249 def __init__(self, args):
250 # redirect stdout/stderr to our logfile
251 if LOGFILE:
252 # appending, unbuffered
253 sys.stdout = sys.stderr = open(LOGFILE, 'a', 0)
254 win32serviceutil.ServiceFramework.__init__(self, args)
255 BaseHTTPServer.HTTPServer.__init__(self, self.address,
256 RoundupRequestHandler)
258 # Create the necessary NT Event synchronization objects...
259 # hevSvcStop is signaled when the SCM sends us a notification
260 # to shutdown the service.
261 self.hevSvcStop = win32event.CreateEvent(None, 0, 0, None)
263 # hevConn is signaled when we have a new incomming connection.
264 self.hevConn = win32event.CreateEvent(None, 0, 0, None)
266 # Hang onto this module for other people to use for logging
267 # purposes.
268 import servicemanager
269 self.servicemanager = servicemanager
271 def SvcStop(self):
272 # Before we do anything, tell the SCM we are starting the
273 # stop process.
274 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
275 win32event.SetEvent(self.hevSvcStop)
277 def SvcDoRun(self):
278 try:
279 self.serve_forever()
280 except SvcShutdown:
281 pass
283 def get_request(self):
284 # Call WSAEventSelect to enable self.socket to be waited on.
285 WSAEventSelect(self.socket, self.hevConn, FD_ACCEPT)
286 while 1:
287 try:
288 rv = self.socket.accept()
289 except socket.error, why:
290 if why[0] != WSAEWOULDBLOCK:
291 raise
292 # Use WaitForMultipleObjects instead of select() because
293 # on NT select() is only good for sockets, and not general
294 # NT synchronization objects.
295 rc = WaitForMultipleObjects((self.hevSvcStop, self.hevConn),
296 0, INFINITE)
297 if rc == WAIT_OBJECT_0:
298 # self.hevSvcStop was signaled, this means:
299 # Stop the service!
300 # So we throw the shutdown exception, which gets
301 # caught by self.SvcDoRun
302 raise SvcShutdown
303 # Otherwise, rc == WAIT_OBJECT_0 + 1 which means
304 # self.hevConn was signaled, which means when we call
305 # self.socket.accept(), we'll have our incoming connection
306 # socket!
307 # Loop back to the top, and let that accept do its thing...
308 else:
309 # yay! we have a connection
310 # However... the new socket is non-blocking, we need to
311 # set it back into blocking mode. (The socket that accept()
312 # returns has the same properties as the listening sockets,
313 # this includes any properties set by WSAAsyncSelect, or
314 # WSAEventSelect, and whether its a blocking socket or not.)
315 #
316 # So if you yank the following line, the setblocking() call
317 # will be useless. The socket will still be in non-blocking
318 # mode.
319 WSAEventSelect(rv[0], self.hevConn, 0)
320 rv[0].setblocking(1)
321 break
322 return rv
324 def usage(message=''):
325 if RoundupService:
326 win = ''' -c: Windows Service options. If you want to run the server as a Windows
327 Service, you must configure the rest of the options by changing the
328 constants of this program. You will at least configure one tracker
329 in the TRACKER_HOMES variable. This option is mutually exclusive
330 from the rest. Typing "roundup-server -c help" shows Windows
331 Services specifics.'''
332 else:
333 win = ''
334 port=PORT
335 print _('''%(message)s
336 Usage:
337 roundup-server [options] [name=tracker home]*
339 options:
340 -n: sets the host name
341 -p: sets the port to listen on (default: %(port)s)
342 -u: sets the uid to this user after listening on the port
343 -g: sets the gid to this group after listening on the port
344 -l: sets a filename to log to (instead of stderr / stdout)
345 -d: run the server in the background and on UN*X write the server's PID
346 to the nominated file. The -l option *must* be specified if this
347 option is.
348 -N: log client machine names in access log instead of IP addresses (much
349 slower)
350 %(win)s
352 name=tracker home:
353 Sets the tracker home(s) to use. The name is how the tracker is
354 identified in the URL (it's the first part of the URL path). The
355 tracker home is the directory that was identified when you did
356 "roundup-admin init". You may specify any number of these name=home
357 pairs on the command-line. For convenience, you may edit the
358 TRACKER_HOMES variable in the roundup-server file instead.
359 Make sure the name part doesn't include any url-unsafe characters like
360 spaces, as these confuse the cookie handling in browsers like IE.
361 ''')%locals()
362 sys.exit(0)
364 def daemonize(pidfile):
365 ''' Turn this process into a daemon.
366 - make sure the sys.std(in|out|err) are completely cut off
367 - make our parent PID 1
369 Write our new PID to the pidfile.
371 From A.M. Kuuchling (possibly originally Greg Ward) with
372 modification from Oren Tirosh, and finally a small mod from me.
373 '''
374 # Fork once
375 if os.fork() != 0:
376 os._exit(0)
378 # Create new session
379 os.setsid()
381 # Second fork to force PPID=1
382 pid = os.fork()
383 if pid:
384 pidfile = open(pidfile, 'w')
385 pidfile.write(str(pid))
386 pidfile.close()
387 os._exit(0)
389 os.chdir("/")
390 os.umask(0)
392 # close off sys.std(in|out|err), redirect to devnull so the file
393 # descriptors can't be used again
394 devnull = os.open('/dev/null', 0)
395 os.dup2(devnull, 0)
396 os.dup2(devnull, 1)
397 os.dup2(devnull, 2)
399 def run(port=PORT, success_message=None):
400 ''' Script entry point - handle args and figure out what to to.
401 '''
402 # time out after a minute if we can
403 import socket
404 if hasattr(socket, 'setdefaulttimeout'):
405 socket.setdefaulttimeout(60)
407 hostname = HOSTNAME
408 pidfile = PIDFILE
409 logfile = LOGFILE
410 user = ROUNDUP_USER
411 group = ROUNDUP_GROUP
412 svc_args = None
414 try:
415 # handle the command-line args
416 options = 'n:p:u:d:l:hN'
417 if RoundupService:
418 options += 'c'
420 try:
421 optlist, args = getopt.getopt(sys.argv[1:], options)
422 except getopt.GetoptError, e:
423 usage(str(e))
425 user = ROUNDUP_USER
426 group = None
427 for (opt, arg) in optlist:
428 if opt == '-n': hostname = arg
429 elif opt == '-p': port = int(arg)
430 elif opt == '-u': user = arg
431 elif opt == '-g': group = arg
432 elif opt == '-d': pidfile = os.path.abspath(arg)
433 elif opt == '-l': logfile = os.path.abspath(arg)
434 elif opt == '-h': usage()
435 elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
436 elif opt == '-c': svc_args = [opt] + args; args = None
438 if svc_args is not None and len(optlist) > 1:
439 raise ValueError, _("windows service option must be the only one")
441 if pidfile and not logfile:
442 raise ValueError, _("logfile *must* be specified if pidfile is")
444 # obtain server before changing user id - allows to use port <
445 # 1024 if started as root
446 address = (hostname, port)
447 try:
448 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
449 except socket.error, e:
450 if e[0] == errno.EADDRINUSE:
451 raise socket.error, \
452 _("Unable to bind to port %s, port already in use." % port)
453 raise
455 if group is not None and hasattr(os, 'getgid'):
456 # if root, setgid to the running user
457 if not os.getgid() and user is not None:
458 try:
459 import pwd
460 except ImportError:
461 raise ValueError, _("Can't change groups - no pwd module")
462 try:
463 gid = pwd.getpwnam(user)[3]
464 except KeyError:
465 raise ValueError,_("Group %(group)s doesn't exist")%locals()
466 os.setgid(gid)
467 elif os.getgid() and user is not None:
468 print _('WARNING: ignoring "-g" argument, not root')
470 if hasattr(os, 'getuid'):
471 # if root, setuid to the running user
472 if not os.getuid() and user is not None:
473 try:
474 import pwd
475 except ImportError:
476 raise ValueError, _("Can't change users - no pwd module")
477 try:
478 uid = pwd.getpwnam(user)[2]
479 except KeyError:
480 raise ValueError, _("User %(user)s doesn't exist")%locals()
481 os.setuid(uid)
482 elif os.getuid() and user is not None:
483 print _('WARNING: ignoring "-u" argument, not root')
485 # People can remove this check if they're really determined
486 if not os.getuid() and user is None:
487 raise ValueError, _("Can't run as root!")
489 # handle tracker specs
490 if args:
491 d = {}
492 for arg in args:
493 try:
494 name, home = arg.split('=')
495 except ValueError:
496 raise ValueError, _("Instances must be name=home")
497 d[name] = os.path.abspath(home)
498 RoundupRequestHandler.TRACKER_HOMES = d
499 except SystemExit:
500 raise
501 except ValueError:
502 usage(error())
503 except:
504 print error()
505 sys.exit(1)
507 # we don't want the cgi module interpreting the command-line args ;)
508 sys.argv = sys.argv[:1]
510 if pidfile:
511 if not hasattr(os, 'fork'):
512 print "Sorry, you can't run the server as a daemon on this" \
513 'Operating System'
514 sys.exit(0)
515 else:
516 daemonize(pidfile)
518 if svc_args is not None:
519 # don't do any other stuff
520 return win32serviceutil.HandleCommandLine(RoundupService, argv=svc_args)
522 # redirect stdout/stderr to our logfile
523 if logfile:
524 # appending, unbuffered
525 sys.stdout = sys.stderr = open(logfile, 'a', 0)
527 if success_message:
528 print success_message
529 else:
530 print _('Roundup server started on %(address)s')%locals()
532 try:
533 httpd.serve_forever()
534 except KeyboardInterrupt:
535 print 'Keyboard Interrupt: exiting'
537 if __name__ == '__main__':
538 run()
540 # vim: set filetype=python ts=4 sw=4 et si