Code

- nicer display of tracker list in roundup-server (sf bug 619769)
[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.14 2002-10-08 03:31:09 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
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     'bar': '/tmp/bar',
44 }
46 ROUNDUP_USER = None
49 # Where to log debugging information to. Use an instance of DevNull if you
50 # don't want to log anywhere.
51 # TODO: actually use this stuff
52 #class DevNull:
53 #    def write(self, info):
54 #        pass
55 #LOG = open('/var/log/roundup.cgi.log', 'a')
56 #LOG = DevNull()
58 #
59 ##  end configuration
60 #
62 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
63     TRACKER_HOMES = TRACKER_HOMES
64     ROUNDUP_USER = ROUNDUP_USER
66     def run_cgi(self):
67         """ Execute the CGI command. Wrap an innner call in an error
68             handler so all errors can be caught.
69         """
70         save_stdin = sys.stdin
71         sys.stdin = self.rfile
72         try:
73             self.inner_run_cgi()
74         except client.NotFound:
75             self.send_error(404, self.path)
76         except client.Unauthorised:
77             self.send_error(403, self.path)
78         except:
79             # it'd be nice to be able to detect if these are going to have
80             # any effect...
81             self.send_response(400)
82             self.send_header('Content-Type', 'text/html')
83             self.end_headers()
84             try:
85                 reload(cgitb)
86                 self.wfile.write(cgitb.breaker())
87                 self.wfile.write(cgitb.html())
88             except:
89                 self.wfile.write("<pre>")
90                 s = StringIO.StringIO()
91                 traceback.print_exc(None, s)
92                 self.wfile.write(cgi.escape(s.getvalue()))
93                 self.wfile.write("</pre>\n")
94         sys.stdin = save_stdin
96     do_GET = do_POST = do_HEAD = send_head = run_cgi
98     def index(self):
99         ''' Print up an index of the available trackers
100         '''
101         self.send_response(200)
102         self.send_header('Content-Type', 'text/html')
103         self.end_headers()
104         w = self.wfile.write
105         w(_('<html><head><title>Roundup trackers index</title></head>\n'))
106         w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
107         keys = self.TRACKER_HOMES.keys()
108         keys.sort()
109         for tracker in keys:
110             w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
111                 'tracker_url': urllib.quote(tracker),
112                 'tracker_name': cgi.escape(tracker)})
113         w(_('</ol></body></html>'))
115     def inner_run_cgi(self):
116         ''' This is the inner part of the CGI handling
117         '''
119         rest = self.path
120         i = rest.rfind('?')
121         if i >= 0:
122             rest, query = rest[:i], rest[i+1:]
123         else:
124             query = ''
126         # figure the tracker
127         if rest == '/':
128             return self.index()
129         l_path = rest.split('/')
130         tracker_name = urllib.unquote(l_path[1])
131         if self.TRACKER_HOMES.has_key(tracker_name):
132             tracker_home = self.TRACKER_HOMES[tracker_name]
133             tracker = roundup.instance.open(tracker_home)
134         else:
135             raise client.NotFound
137         # figure out what the rest of the path is
138         if len(l_path) > 2:
139             rest = '/'.join(l_path[2:])
140         else:
141             rest = '/'
143         # Set up the CGI environment
144         env = {}
145         env['TRACKER_NAME'] = tracker_name
146         env['REQUEST_METHOD'] = self.command
147         env['PATH_INFO'] = urllib.unquote(rest)
148         if query:
149             env['QUERY_STRING'] = query
150         host = self.address_string()
151         if self.headers.typeheader is None:
152             env['CONTENT_TYPE'] = self.headers.type
153         else:
154             env['CONTENT_TYPE'] = self.headers.typeheader
155         length = self.headers.getheader('content-length')
156         if length:
157             env['CONTENT_LENGTH'] = length
158         co = filter(None, self.headers.getheaders('cookie'))
159         if co:
160             env['HTTP_COOKIE'] = ', '.join(co)
161         env['SCRIPT_NAME'] = ''
162         env['SERVER_NAME'] = self.server.server_name
163         env['SERVER_PORT'] = str(self.server.server_port)
164         env['HTTP_HOST'] = self.headers['host']
166         decoded_query = query.replace('+', ' ')
168         # do the roundup thang
169         c = tracker.Client(tracker, self, env)
170         c.main()
172 def usage(message=''):
173     if message:
174         message = _('Error: %(error)s\n\n')%{'error': message}
175     print _('''%(message)sUsage:
176 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=tracker home]*
178  -n: sets the host name
179  -p: sets the port to listen on
180  -l: sets a filename to log to (instead of stdout)
181  -d: daemonize, and write the server's PID to the nominated file
183  name=tracker home
184    Sets the tracker home(s) to use. The name is how the tracker is
185    identified in the URL (it's the first part of the URL path). The
186    tracker home is the directory that was identified when you did
187    "roundup-admin init". You may specify any number of these name=home
188    pairs on the command-line. For convenience, you may edit the
189    TRACKER_HOMES variable in the roundup-server file instead.
190    Make sure the name part doesn't include any url-unsafe characters like 
191    spaces, as these confuse the cookie handling in browsers like IE.
192 ''')%locals()
193     sys.exit(0)
195 def daemonize(pidfile):
196     ''' Turn this process into a daemon.
197         - make sure the sys.std(in|out|err) are completely cut off
198         - make our parent PID 1
200         Write our new PID to the pidfile.
202         From A.M. Kuuchling (possibly originally Greg Ward) with
203         modification from Oren Tirosh, and finally a small mod from me.
204     '''
205     # Fork once
206     if os.fork() != 0:
207         os._exit(0)
209     # Create new session
210     os.setsid()
212     # Second fork to force PPID=1
213     pid = os.fork()
214     if pid:
215         pidfile = open(pidfile, 'w')
216         pidfile.write(str(pid))
217         pidfile.close()
218         os._exit(0)         
220     os.chdir("/")         
221     os.umask(0)
223     # close off sys.std(in|out|err), redirect to devnull so the file
224     # descriptors can't be used again
225     devnull = os.open('/dev/null', 0)
226     os.dup2(devnull, 0)
227     os.dup2(devnull, 1)
228     os.dup2(devnull, 2)
230 def abspath(path):
231     ''' Make the given path an absolute path.
233         Code from Zope-Coders posting of 2002-10-06 by GvR.
234     '''
235     if not os.path.isabs(path):
236         path = os.path.join(os.getcwd(), path)
237     return os.path.normpath(path)
239 def run():
240     ''' Script entry point - handle args and figure out what to to.
241     '''
242     hostname = ''
243     port = 8080
244     pidfile = None
245     logfile = None
246     try:
247         # handle the command-line args
248         try:
249             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:')
250         except getopt.GetoptError, e:
251             usage(str(e))
253         user = ROUNDUP_USER
254         for (opt, arg) in optlist:
255             if opt == '-n': hostname = arg
256             elif opt == '-p': port = int(arg)
257             elif opt == '-u': user = arg
258             elif opt == '-d': pidfile = abspath(arg)
259             elif opt == '-l': logfile = abspath(arg)
260             elif opt == '-h': usage()
262         if hasattr(os, 'getuid'):
263             # if root, setuid to the running user
264             if not os.getuid() and user is not None:
265                 try:
266                     import pwd
267                 except ImportError:
268                     raise ValueError, _("Can't change users - no pwd module")
269                 try:
270                     uid = pwd.getpwnam(user)[2]
271                 except KeyError:
272                     raise ValueError, _("User %(user)s doesn't exist")%locals()
273                 os.setuid(uid)
274             elif os.getuid() and user is not None:
275                 print _('WARNING: ignoring "-u" argument, not root')
277             # People can remove this check if they're really determined
278             if not os.getuid() and user is None:
279                 raise ValueError, _("Can't run as root!")
281         # handle tracker specs
282         if args:
283             d = {}
284             for arg in args:
285                 try:
286                     name, home = arg.split('=')
287                 except ValueError:
288                     raise ValueError, _("Instances must be name=home")
289                 d[name] = home
290             RoundupRequestHandler.TRACKER_HOMES = d
291     except SystemExit:
292         raise
293     except:
294         exc_type, exc_value = sys.exc_info()[:2]
295         usage('%s: %s'%(exc_type, exc_value))
297     # we don't want the cgi module interpreting the command-line args ;)
298     sys.argv = sys.argv[:1]
299     address = (hostname, port)
301     # fork?
302     if pidfile:
303         daemonize(pidfile)
305     # redirect stdout/stderr to our logfile
306     if logfile:
307         sys.stdout = sys.stderr = open(logfile, 'a')
309     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
310     print _('Roundup server started on %(address)s')%locals()
311     httpd.serve_forever()
313 if __name__ == '__main__':
314     run()
316 # vim: set filetype=python ts=4 sw=4 et si