Code

added socket timeout to attempt to prevent stuck processes (sf bug 665487)
[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.22 2003-04-24 04:27:32 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     # time out after a minute if we can
258     import socket
259     if hasattr(socket, 'setdefaulttimeout'):
260         socket.setdefaulttimeout(60)
262     hostname = ''
263     port = 8080
264     pidfile = None
265     logfile = None
266     try:
267         # handle the command-line args
268         try:
269             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:h')
270         except getopt.GetoptError, e:
271             usage(str(e))
273         user = ROUNDUP_USER
274         for (opt, arg) in optlist:
275             if opt == '-n': hostname = arg
276             elif opt == '-p': port = int(arg)
277             elif opt == '-u': user = arg
278             elif opt == '-d': pidfile = abspath(arg)
279             elif opt == '-l': logfile = abspath(arg)
280             elif opt == '-h': usage()
282         if hasattr(os, 'getuid'):
283             # if root, setuid to the running user
284             if not os.getuid() and user is not None:
285                 try:
286                     import pwd
287                 except ImportError:
288                     raise ValueError, _("Can't change users - no pwd module")
289                 try:
290                     uid = pwd.getpwnam(user)[2]
291                 except KeyError:
292                     raise ValueError, _("User %(user)s doesn't exist")%locals()
293                 os.setuid(uid)
294             elif os.getuid() and user is not None:
295                 print _('WARNING: ignoring "-u" argument, not root')
297             # People can remove this check if they're really determined
298             if not os.getuid() and user is None:
299                 raise ValueError, _("Can't run as root!")
301         # handle tracker specs
302         if args:
303             d = {}
304             for arg in args:
305                 try:
306                     name, home = arg.split('=')
307                 except ValueError:
308                     raise ValueError, _("Instances must be name=home")
309                 d[name] = home
310             RoundupRequestHandler.TRACKER_HOMES = d
311     except SystemExit:
312         raise
313     except:
314         exc_type, exc_value = sys.exc_info()[:2]
315         usage('%s: %s'%(exc_type, exc_value))
317     # we don't want the cgi module interpreting the command-line args ;)
318     sys.argv = sys.argv[:1]
319     address = (hostname, port)
321     # fork?
322     if pidfile:
323         if not hasattr(os, 'fork'):
324             print "Sorry, you can't run the server as a daemon on this" \
325                 'Operating System'
326             sys.exit(0)
327         daemonize(pidfile)
329     # redirect stdout/stderr to our logfile
330     if logfile:
331         # appending, unbuffered
332         sys.stdout = sys.stderr = open(logfile, 'a', 0)
334     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
335     print _('Roundup server started on %(address)s')%locals()
336     try:
337         httpd.serve_forever()
338     except KeyboardInterrupt:
339         print 'Keyboard Interrupt: exiting'
341 if __name__ == '__main__':
342     run()
344 # vim: set filetype=python ts=4 sw=4 et si