Code

some updates that were sitting on disk
[roundup.git] / roundup / scripts / roundup_server.py
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
17 """ HTTP Server that serves roundup.
19 $Id: roundup_server.py,v 1.35 2003-12-04 02:43:07 richard Exp $
20 """
22 # python version check
23 from roundup import version_check
25 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
26 import BaseHTTPServer, socket, errno
28 # Roundup modules of use here
29 from roundup.cgi import cgitb, client
30 import roundup.instance
31 from roundup.i18n import _
33 #
34 ##  Configuration
35 #
37 # This indicates where the Roundup trackers live. They're given as NAME ->
38 # TRACKER_HOME, where the NAME part is used in the URL to select the
39 # appropriate reacker.
40 # Make sure the NAME part doesn't include any url-unsafe characters like 
41 # spaces, as these confuse the cookie handling in browsers like IE.
42 TRACKER_HOMES = {
43 #    'example': '/path/to/example',
44 }
46 ROUNDUP_USER = None
49 #
50 ##  end configuration
51 #
53 import zlib, base64
54 favico = zlib.decompress(base64.decodestring('''
55 eJyVUk2IQVEUfn4yaRYjibdQZiVba/ZE2djIUmHWFjaKGVmIlY2iFMVG2ViQhXqFSP6iFFJvw4uF
56 LGdWd743mpeMn+a88917Oue7955z3qEoET6FQkHx8iahKDV2A8B7XgERRf/EKMSUzyf8ypbbnnQy
57 mWBdr9eVSkVw3tJGoxGNRpvNZigUyufzWPv9Pvwcx0UiERj7/V4g73Y7j8fTarWMRmO73U4kEkKI
58 YZhardbr9eLxuOD0+/2ZTMZisYjFYpqmU6kU799uN5tNMBg8HA7ZbPY8GaTh8/mEipRKpclk0ul0
59 NpvNarUmk0mWZS/yr9frcrmc+iMOh+NWydPp1Ov1SiSSc344HL7fKKfTiSN2u12tVqOcxWJxn6/V
60 ag0GAwxkrlKp5vP5fT7ulMlk6XRar9dLpVIUXi6Xb5Hxa1wul0ajKZVKsVjM7XYXCoVOp3OVPJvN
61 AoFAtVo1m825XO7hSODOYrH4kHbxxGAwwODBGI/H6DBs5LNara7yl8slGjIcDsHpdrunU6PRCAP2
62 r3fPdUcIYeyEfLSAJ0LeAUZHCAt8Al/8/kLIEWDB5YDj0wm8fAP6fVfo
63 '''.strip()))
65 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
66     TRACKER_HOMES = TRACKER_HOMES
67     ROUNDUP_USER = ROUNDUP_USER
69     def run_cgi(self):
70         """ Execute the CGI command. Wrap an innner call in an error
71             handler so all errors can be caught.
72         """
73         save_stdin = sys.stdin
74         sys.stdin = self.rfile
75         try:
76             self.inner_run_cgi()
77         except client.NotFound:
78             self.send_error(404, self.path)
79         except client.Unauthorised:
80             self.send_error(403, self.path)
81         except:
82             exc, val, tb = sys.exc_info()
83             if hasattr(socket, 'timeout') and exc == socket.timeout:
84                 s = StringIO.StringIO()
85                 traceback.print_exc(None, s)
86                 self.log_message(str(s.getvalue()))
87             else:
88                 # it'd be nice to be able to detect if these are going to have
89                 # any effect...
90                 self.send_response(400)
91                 self.send_header('Content-Type', 'text/html')
92                 self.end_headers()
93                 try:
94                     reload(cgitb)
95                     self.wfile.write(cgitb.breaker())
96                     self.wfile.write(cgitb.html())
97                 except:
98                     s = StringIO.StringIO()
99                     traceback.print_exc(None, s)
100                     self.wfile.write("<pre>")
101                     self.wfile.write(cgi.escape(s.getvalue()))
102                     self.wfile.write("</pre>\n")
103         sys.stdin = save_stdin
105     do_GET = do_POST = run_cgi
107     def index(self):
108         ''' Print up an index of the available trackers
109         '''
110         self.send_response(200)
111         self.send_header('Content-Type', 'text/html')
112         self.end_headers()
113         w = self.wfile.write
114         w(_('<html><head><title>Roundup trackers index</title></head>\n'))
115         w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
116         keys = self.TRACKER_HOMES.keys()
117         keys.sort()
118         for tracker in keys:
119             w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
120                 'tracker_url': urllib.quote(tracker),
121                 'tracker_name': cgi.escape(tracker)})
122         w(_('</ol></body></html>'))
124     def inner_run_cgi(self):
125         ''' This is the inner part of the CGI handling
126         '''
127         rest = self.path
129         if rest == '/favicon.ico':
130             raise client.NotFound
132         i = rest.rfind('?')
133         if i >= 0:
134             rest, query = rest[:i], rest[i+1:]
135         else:
136             query = ''
138         # no tracker - spit out the index
139         if rest == '/':
140             return self.index()
142         # figure the tracker
143         l_path = rest.split('/')
144         tracker_name = urllib.unquote(l_path[1])
146         # handle missing trailing '/'
147         if len(l_path) == 2:
148             self.send_response(301)
149             # redirect - XXX https??
150             protocol = 'http'
151             url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
152             self.send_header('Location', url)
153             self.end_headers()
154             self.wfile.write('Moved Permanently')
155             return
157         if self.TRACKER_HOMES.has_key(tracker_name):
158             tracker_home = self.TRACKER_HOMES[tracker_name]
159             tracker = roundup.instance.open(tracker_home)
160         else:
161             raise client.NotFound
163         # figure out what the rest of the path is
164         if len(l_path) > 2:
165             rest = '/'.join(l_path[2:])
166         else:
167             rest = '/'
169         # Set up the CGI environment
170         env = {}
171         env['TRACKER_NAME'] = tracker_name
172         env['REQUEST_METHOD'] = self.command
173         env['PATH_INFO'] = urllib.unquote(rest)
174         if query:
175             env['QUERY_STRING'] = query
176         host = self.address_string()
177         if self.headers.typeheader is None:
178             env['CONTENT_TYPE'] = self.headers.type
179         else:
180             env['CONTENT_TYPE'] = self.headers.typeheader
181         length = self.headers.getheader('content-length')
182         if length:
183             env['CONTENT_LENGTH'] = length
184         co = filter(None, self.headers.getheaders('cookie'))
185         if co:
186             env['HTTP_COOKIE'] = ', '.join(co)
187         env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
188         env['SCRIPT_NAME'] = ''
189         env['SERVER_NAME'] = self.server.server_name
190         env['SERVER_PORT'] = str(self.server.server_port)
191         env['HTTP_HOST'] = self.headers['host']
193         decoded_query = query.replace('+', ' ')
195         # do the roundup thang
196         c = tracker.Client(tracker, self, env)
197         c.main()
199     LOG_IPADDRESS = 1
200     def address_string(self):
201         if self.LOG_IPADDRESS:
202             return self.client_address[0]
203         else:
204             host, port = self.client_address
205             return socket.getfqdn(host)
207 def error():
208     exc_type, exc_value = sys.exc_info()[:2]
209     return _('Error: %s: %s' % (exc_type, exc_value))
211 def usage(message=''):
212     print _('''%(message)s
214 Usage:
215 roundup-server [options] [name=tracker home]*
217 options:
218  -n: sets the host name
219  -p: sets the port to listen on
220  -u: sets the uid to this user after listening on the port
221  -g: sets the gid to this group after listening on the port
222  -l: sets a filename to log to (instead of stdout)
223  -d: sets a filename to write server PID to. This option causes the server 
224      to run in the background. Note: on Windows the PID argument is needed,
225      but ignored. The -l option *must* be specified if this option is.
226  -N: log client machine names in access log instead of IP addresses (much
227      slower)
229 name=tracker home:
230    Sets the tracker home(s) to use. The name is how the tracker is
231    identified in the URL (it's the first part of the URL path). The
232    tracker home is the directory that was identified when you did
233    "roundup-admin init". You may specify any number of these name=home
234    pairs on the command-line. For convenience, you may edit the
235    TRACKER_HOMES variable in the roundup-server file instead.
236    Make sure the name part doesn't include any url-unsafe characters like 
237    spaces, as these confuse the cookie handling in browsers like IE.
238 ''')%locals()
239     sys.exit(0)
241 def daemonize(pidfile):
242     ''' Turn this process into a daemon.
243         - make sure the sys.std(in|out|err) are completely cut off
244         - make our parent PID 1
246         Write our new PID to the pidfile.
248         From A.M. Kuuchling (possibly originally Greg Ward) with
249         modification from Oren Tirosh, and finally a small mod from me.
250     '''
251     # Fork once
252     if os.fork() != 0:
253         os._exit(0)
255     # Create new session
256     os.setsid()
258     # Second fork to force PPID=1
259     pid = os.fork()
260     if pid:
261         pidfile = open(pidfile, 'w')
262         pidfile.write(str(pid))
263         pidfile.close()
264         os._exit(0)         
266     os.chdir("/")         
267     os.umask(0)
269     # close off sys.std(in|out|err), redirect to devnull so the file
270     # descriptors can't be used again
271     devnull = os.open('/dev/null', 0)
272     os.dup2(devnull, 0)
273     os.dup2(devnull, 1)
274     os.dup2(devnull, 2)
276 def run(port=8080, success_message=None):
277     ''' Script entry point - handle args and figure out what to to.
278     '''
279     # time out after a minute if we can
280     import socket
281     if hasattr(socket, 'setdefaulttimeout'):
282         socket.setdefaulttimeout(60)
284     hostname = ''
285     pidfile = None
286     logfile = None
287     try:
288         # handle the command-line args
289         try:
290             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:hN')
291         except getopt.GetoptError, e:
292             usage(str(e))
294         user = ROUNDUP_USER
295         group = None
296         for (opt, arg) in optlist:
297             if opt == '-n': hostname = arg
298             elif opt == '-p': port = int(arg)
299             elif opt == '-u': user = arg
300             elif opt == '-g': group = arg
301             elif opt == '-d': pidfile = os.path.abspath(arg)
302             elif opt == '-l': logfile = os.path.abspath(arg)
303             elif opt == '-h': usage()
304             elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
306         if pidfile and not logfile:
307             raise ValueError, _("logfile *must* be specified if pidfile is")
308   
309         # obtain server before changing user id - allows to use port <
310         # 1024 if started as root
311         address = (hostname, port)
312         try:
313             httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
314         except socket.error, e:
315             if e[0] == errno.EADDRINUSE:
316                 raise socket.error, \
317                       _("Unable to bind to port %s, port already in use." % port)
318             raise
320         if group is not None and hasattr(os, 'getgid'):
321             # if root, setgid to the running user
322             if not os.getgid() and user is not None:
323                 try:
324                     import pwd
325                 except ImportError:
326                     raise ValueError, _("Can't change groups - no pwd module")
327                 try:
328                     gid = pwd.getpwnam(user)[3]
329                 except KeyError:
330                     raise ValueError,_("Group %(group)s doesn't exist")%locals()
331                 os.setgid(gid)
332             elif os.getgid() and user is not None:
333                 print _('WARNING: ignoring "-g" argument, not root')
335         if hasattr(os, 'getuid'):
336             # if root, setuid to the running user
337             if not os.getuid() and user is not None:
338                 try:
339                     import pwd
340                 except ImportError:
341                     raise ValueError, _("Can't change users - no pwd module")
342                 try:
343                     uid = pwd.getpwnam(user)[2]
344                 except KeyError:
345                     raise ValueError, _("User %(user)s doesn't exist")%locals()
346                 os.setuid(uid)
347             elif os.getuid() and user is not None:
348                 print _('WARNING: ignoring "-u" argument, not root')
350             # People can remove this check if they're really determined
351             if not os.getuid() and user is None:
352                 raise ValueError, _("Can't run as root!")
354         # handle tracker specs
355         if args:
356             d = {}
357             for arg in args:
358                 try:
359                     name, home = arg.split('=')
360                 except ValueError:
361                     raise ValueError, _("Instances must be name=home")
362                 d[name] = home
363             RoundupRequestHandler.TRACKER_HOMES = d
364     except SystemExit:
365         raise
366     except ValueError:
367         usage(error())
368     except:
369         print error()
370         sys.exit(1)
372     # we don't want the cgi module interpreting the command-line args ;)
373     sys.argv = sys.argv[:1]
375     if pidfile:
376         if not hasattr(os, 'fork'):
377             print "Sorry, you can't run the server as a daemon on this" \
378                 'Operating System'
379             sys.exit(0)
380         else:
381             daemonize(pidfile)
383     # redirect stdout/stderr to our logfile
384     if logfile:
385         # appending, unbuffered
386         sys.stdout = sys.stderr = open(logfile, 'a', 0)
388     if success_message:
389         print success_message
390     else:
391         print _('Roundup server started on %(address)s')%locals()
393     try:
394         httpd.serve_forever()
395     except KeyboardInterrupt:
396         print 'Keyboard Interrupt: exiting'
398 if __name__ == '__main__':
399     run()
401 # vim: set filetype=python ts=4 sw=4 et si