Code

we don't support HEAD
[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.21 2003-03-26 04:54:59 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                 s = StringIO.StringIO()
90                 traceback.print_exc(None, s)
91                 self.wfile.write("<pre>")
92                 self.wfile.write(cgi.escape(s.getvalue()))
93                 self.wfile.write("</pre>\n")
94         sys.stdin = save_stdin
96     do_GET = do_POST = 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         # no tracker - spit out the index
127         if rest == '/':
128             return self.index()
130         # figure the tracker
131         l_path = rest.split('/')
132         tracker_name = urllib.unquote(l_path[1])
134         # handle missing trailing '/'
135         if len(l_path) == 2:
136             self.send_response(301)
137             # redirect - XXX https??
138             protocol = 'http'
139             url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
140             self.send_header('Location', url)
141             self.end_headers()
142             self.wfile.write('Moved Permanently')
143             return
145         if self.TRACKER_HOMES.has_key(tracker_name):
146             tracker_home = self.TRACKER_HOMES[tracker_name]
147             tracker = roundup.instance.open(tracker_home)
148         else:
149             raise client.NotFound
151         # figure out what the rest of the path is
152         if len(l_path) > 2:
153             rest = '/'.join(l_path[2:])
154         else:
155             rest = '/'
157         # Set up the CGI environment
158         env = {}
159         env['TRACKER_NAME'] = tracker_name
160         env['REQUEST_METHOD'] = self.command
161         env['PATH_INFO'] = urllib.unquote(rest)
162         if query:
163             env['QUERY_STRING'] = query
164         host = self.address_string()
165         if self.headers.typeheader is None:
166             env['CONTENT_TYPE'] = self.headers.type
167         else:
168             env['CONTENT_TYPE'] = self.headers.typeheader
169         length = self.headers.getheader('content-length')
170         if length:
171             env['CONTENT_LENGTH'] = length
172         co = filter(None, self.headers.getheaders('cookie'))
173         if co:
174             env['HTTP_COOKIE'] = ', '.join(co)
175         env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
176         env['SCRIPT_NAME'] = ''
177         env['SERVER_NAME'] = self.server.server_name
178         env['SERVER_PORT'] = str(self.server.server_port)
179         env['HTTP_HOST'] = self.headers['host']
181         decoded_query = query.replace('+', ' ')
183         # do the roundup thang
184         c = tracker.Client(tracker, self, env)
185         c.main()
187 def usage(message=''):
188     if message:
189         message = _('Error: %(error)s\n\n')%{'error': message}
190     print _('''%(message)sUsage:
191 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=tracker home]*
193  -n: sets the host name
194  -p: sets the port to listen on
195  -l: sets a filename to log to (instead of stdout)
196  -d: daemonize, and write the server's PID to the nominated file
198  name=tracker home
199    Sets the tracker home(s) to use. The name is how the tracker is
200    identified in the URL (it's the first part of the URL path). The
201    tracker home is the directory that was identified when you did
202    "roundup-admin init". You may specify any number of these name=home
203    pairs on the command-line. For convenience, you may edit the
204    TRACKER_HOMES variable in the roundup-server file instead.
205    Make sure the name part doesn't include any url-unsafe characters like 
206    spaces, as these confuse the cookie handling in browsers like IE.
207 ''')%locals()
208     sys.exit(0)
210 def daemonize(pidfile):
211     ''' Turn this process into a daemon.
212         - make sure the sys.std(in|out|err) are completely cut off
213         - make our parent PID 1
215         Write our new PID to the pidfile.
217         From A.M. Kuuchling (possibly originally Greg Ward) with
218         modification from Oren Tirosh, and finally a small mod from me.
219     '''
220     # Fork once
221     if os.fork() != 0:
222         os._exit(0)
224     # Create new session
225     os.setsid()
227     # Second fork to force PPID=1
228     pid = os.fork()
229     if pid:
230         pidfile = open(pidfile, 'w')
231         pidfile.write(str(pid))
232         pidfile.close()
233         os._exit(0)         
235     os.chdir("/")         
236     os.umask(0)
238     # close off sys.std(in|out|err), redirect to devnull so the file
239     # descriptors can't be used again
240     devnull = os.open('/dev/null', 0)
241     os.dup2(devnull, 0)
242     os.dup2(devnull, 1)
243     os.dup2(devnull, 2)
245 def abspath(path):
246     ''' Make the given path an absolute path.
248         Code from Zope-Coders posting of 2002-10-06 by GvR.
249     '''
250     if not os.path.isabs(path):
251         path = os.path.join(os.getcwd(), path)
252     return os.path.normpath(path)
254 def run():
255     ''' Script entry point - handle args and figure out what to to.
256     '''
257     hostname = ''
258     port = 8080
259     pidfile = None
260     logfile = None
261     try:
262         # handle the command-line args
263         try:
264             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:h')
265         except getopt.GetoptError, e:
266             usage(str(e))
268         user = ROUNDUP_USER
269         for (opt, arg) in optlist:
270             if opt == '-n': hostname = arg
271             elif opt == '-p': port = int(arg)
272             elif opt == '-u': user = arg
273             elif opt == '-d': pidfile = abspath(arg)
274             elif opt == '-l': logfile = abspath(arg)
275             elif opt == '-h': usage()
277         if hasattr(os, 'getuid'):
278             # if root, setuid to the running user
279             if not os.getuid() and user is not None:
280                 try:
281                     import pwd
282                 except ImportError:
283                     raise ValueError, _("Can't change users - no pwd module")
284                 try:
285                     uid = pwd.getpwnam(user)[2]
286                 except KeyError:
287                     raise ValueError, _("User %(user)s doesn't exist")%locals()
288                 os.setuid(uid)
289             elif os.getuid() and user is not None:
290                 print _('WARNING: ignoring "-u" argument, not root')
292             # People can remove this check if they're really determined
293             if not os.getuid() and user is None:
294                 raise ValueError, _("Can't run as root!")
296         # handle tracker specs
297         if args:
298             d = {}
299             for arg in args:
300                 try:
301                     name, home = arg.split('=')
302                 except ValueError:
303                     raise ValueError, _("Instances must be name=home")
304                 d[name] = home
305             RoundupRequestHandler.TRACKER_HOMES = d
306     except SystemExit:
307         raise
308     except:
309         exc_type, exc_value = sys.exc_info()[:2]
310         usage('%s: %s'%(exc_type, exc_value))
312     # we don't want the cgi module interpreting the command-line args ;)
313     sys.argv = sys.argv[:1]
314     address = (hostname, port)
316     # fork?
317     if pidfile:
318         if not hasattr(os, 'fork'):
319             print "Sorry, you can't run the server as a daemon on this" \
320                 'Operating System'
321             sys.exit(0)
322         daemonize(pidfile)
324     # redirect stdout/stderr to our logfile
325     if logfile:
326         # appending, unbuffered
327         sys.stdout = sys.stderr = open(logfile, 'a', 0)
329     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
330     print _('Roundup server started on %(address)s')%locals()
331     try:
332         httpd.serve_forever()
333     except KeyboardInterrupt:
334         print 'Keyboard Interrupt: exiting'
336 if __name__ == '__main__':
337     run()
339 # vim: set filetype=python ts=4 sw=4 et si