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.38 2004-02-15 21:44:02 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 RoundupHTTPServer(SafeLogging, BaseHTTPServer.HTTPServer):
74 def log_message(self, format, *args):
75 ''' Try to use the logging package, otherwise *safely* log to
76 stderr.
77 '''
78 try:
79 BaseHTTPServer.HTTPServer.log_message(self, format, *args)
80 except IOError:
81 # stderr is no longer viable, we can't log
82 pass
84 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
85 TRACKER_HOMES = TRACKER_HOMES
86 ROUNDUP_USER = ROUNDUP_USER
88 def run_cgi(self):
89 """ Execute the CGI command. Wrap an innner call in an error
90 handler so all errors can be caught.
91 """
92 save_stdin = sys.stdin
93 sys.stdin = self.rfile
94 try:
95 self.inner_run_cgi()
96 except client.NotFound:
97 self.send_error(404, self.path)
98 except client.Unauthorised:
99 self.send_error(403, self.path)
100 except:
101 exc, val, tb = sys.exc_info()
102 if hasattr(socket, 'timeout') and exc == socket.timeout:
103 s = StringIO.StringIO()
104 traceback.print_exc(None, s)
105 self.log_message(str(s.getvalue()))
106 else:
107 # it'd be nice to be able to detect if these are going to have
108 # any effect...
109 self.send_response(400)
110 self.send_header('Content-Type', 'text/html')
111 self.end_headers()
112 try:
113 reload(cgitb)
114 self.wfile.write(cgitb.breaker())
115 self.wfile.write(cgitb.html())
116 except:
117 s = StringIO.StringIO()
118 traceback.print_exc(None, s)
119 self.wfile.write("<pre>")
120 self.wfile.write(cgi.escape(s.getvalue()))
121 self.wfile.write("</pre>\n")
122 sys.stdin = save_stdin
124 do_GET = do_POST = run_cgi
126 def index(self):
127 ''' Print up an index of the available trackers
128 '''
129 self.send_response(200)
130 self.send_header('Content-Type', 'text/html')
131 self.end_headers()
132 w = self.wfile.write
133 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
134 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
135 keys = self.TRACKER_HOMES.keys()
136 keys.sort()
137 for tracker in keys:
138 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
139 'tracker_url': urllib.quote(tracker),
140 'tracker_name': cgi.escape(tracker)})
141 w(_('</ol></body></html>'))
143 def inner_run_cgi(self):
144 ''' This is the inner part of the CGI handling
145 '''
146 rest = self.path
148 if rest == '/favicon.ico':
149 raise client.NotFound
151 i = rest.rfind('?')
152 if i >= 0:
153 rest, query = rest[:i], rest[i+1:]
154 else:
155 query = ''
157 # no tracker - spit out the index
158 if rest == '/':
159 return self.index()
161 # figure the tracker
162 l_path = rest.split('/')
163 tracker_name = urllib.unquote(l_path[1])
165 # handle missing trailing '/'
166 if len(l_path) == 2:
167 self.send_response(301)
168 # redirect - XXX https??
169 protocol = 'http'
170 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
171 self.send_header('Location', url)
172 self.end_headers()
173 self.wfile.write('Moved Permanently')
174 return
176 if self.TRACKER_HOMES.has_key(tracker_name):
177 tracker_home = self.TRACKER_HOMES[tracker_name]
178 tracker = roundup.instance.open(tracker_home)
179 else:
180 raise client.NotFound
182 # figure out what the rest of the path is
183 if len(l_path) > 2:
184 rest = '/'.join(l_path[2:])
185 else:
186 rest = '/'
188 # Set up the CGI environment
189 env = {}
190 env['TRACKER_NAME'] = tracker_name
191 env['REQUEST_METHOD'] = self.command
192 env['PATH_INFO'] = urllib.unquote(rest)
193 if query:
194 env['QUERY_STRING'] = query
195 host = self.address_string()
196 if self.headers.typeheader is None:
197 env['CONTENT_TYPE'] = self.headers.type
198 else:
199 env['CONTENT_TYPE'] = self.headers.typeheader
200 length = self.headers.getheader('content-length')
201 if length:
202 env['CONTENT_LENGTH'] = length
203 co = filter(None, self.headers.getheaders('cookie'))
204 if co:
205 env['HTTP_COOKIE'] = ', '.join(co)
206 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
207 env['SCRIPT_NAME'] = ''
208 env['SERVER_NAME'] = self.server.server_name
209 env['SERVER_PORT'] = str(self.server.server_port)
210 env['HTTP_HOST'] = self.headers['host']
212 decoded_query = query.replace('+', ' ')
214 # do the roundup thang
215 c = tracker.Client(tracker, self, env)
216 c.main()
218 LOG_IPADDRESS = ROUNDUP_LOG_IP
219 def address_string(self):
220 if self.LOG_IPADDRESS:
221 return self.client_address[0]
222 else:
223 host, port = self.client_address
224 return socket.getfqdn(host)
226 def error():
227 exc_type, exc_value = sys.exc_info()[:2]
228 return _('Error: %s: %s' % (exc_type, exc_value))
230 try:
231 import win32serviceutil
232 except:
233 RoundupService = None
234 else:
235 # allow the win32
236 import win32service
237 import win32event
238 from win32event import *
239 from win32file import *
241 SvcShutdown = "ServiceShutdown"
243 class RoundupService(win32serviceutil.ServiceFramework, RoundupHTTPServer):
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 RoundupHTTPServer.__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 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 = RoundupHTTPServer(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