Code

f87ce02f91073929fb1e61fd110c24cb2ce3d52d
[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.31 2003-10-25 11:41:06 jlgijsbers 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
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             # it'd be nice to be able to detect if these are going to have
83             # any effect...
84             self.send_response(400)
85             self.send_header('Content-Type', 'text/html')
86             self.end_headers()
87             try:
88                 reload(cgitb)
89                 self.wfile.write(cgitb.breaker())
90                 self.wfile.write(cgitb.html())
91             except:
92                 s = StringIO.StringIO()
93                 traceback.print_exc(None, s)
94                 self.wfile.write("<pre>")
95                 self.wfile.write(cgi.escape(s.getvalue()))
96                 self.wfile.write("</pre>\n")
97         sys.stdin = save_stdin
99     do_GET = do_POST = run_cgi
101     def index(self):
102         ''' Print up an index of the available trackers
103         '''
104         self.send_response(200)
105         self.send_header('Content-Type', 'text/html')
106         self.end_headers()
107         w = self.wfile.write
108         w(_('<html><head><title>Roundup trackers index</title></head>\n'))
109         w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
110         keys = self.TRACKER_HOMES.keys()
111         keys.sort()
112         for tracker in keys:
113             w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
114                 'tracker_url': urllib.quote(tracker),
115                 'tracker_name': cgi.escape(tracker)})
116         w(_('</ol></body></html>'))
118     def inner_run_cgi(self):
119         ''' This is the inner part of the CGI handling
120         '''
121         rest = self.path
123         if rest == '/favicon.ico':
124             raise client.NotFound
126         i = rest.rfind('?')
127         if i >= 0:
128             rest, query = rest[:i], rest[i+1:]
129         else:
130             query = ''
132         # no tracker - spit out the index
133         if rest == '/':
134             return self.index()
136         # figure the tracker
137         l_path = rest.split('/')
138         tracker_name = urllib.unquote(l_path[1])
140         # handle missing trailing '/'
141         if len(l_path) == 2:
142             self.send_response(301)
143             # redirect - XXX https??
144             protocol = 'http'
145             url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
146             self.send_header('Location', url)
147             self.end_headers()
148             self.wfile.write('Moved Permanently')
149             return
151         if self.TRACKER_HOMES.has_key(tracker_name):
152             tracker_home = self.TRACKER_HOMES[tracker_name]
153             tracker = roundup.instance.open(tracker_home)
154         else:
155             raise client.NotFound
157         # figure out what the rest of the path is
158         if len(l_path) > 2:
159             rest = '/'.join(l_path[2:])
160         else:
161             rest = '/'
163         # Set up the CGI environment
164         env = {}
165         env['TRACKER_NAME'] = tracker_name
166         env['REQUEST_METHOD'] = self.command
167         env['PATH_INFO'] = urllib.unquote(rest)
168         if query:
169             env['QUERY_STRING'] = query
170         host = self.address_string()
171         if self.headers.typeheader is None:
172             env['CONTENT_TYPE'] = self.headers.type
173         else:
174             env['CONTENT_TYPE'] = self.headers.typeheader
175         length = self.headers.getheader('content-length')
176         if length:
177             env['CONTENT_LENGTH'] = length
178         co = filter(None, self.headers.getheaders('cookie'))
179         if co:
180             env['HTTP_COOKIE'] = ', '.join(co)
181         env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
182         env['SCRIPT_NAME'] = ''
183         env['SERVER_NAME'] = self.server.server_name
184         env['SERVER_PORT'] = str(self.server.server_port)
185         env['HTTP_HOST'] = self.headers['host']
187         decoded_query = query.replace('+', ' ')
189         # do the roundup thang
190         c = tracker.Client(tracker, self, env)
191         c.main()
193     LOG_IPADDRESS = 1
194     def address_string(self):
195         if self.LOG_IPADDRESS:
196             return self.client_address[0]
197         else:
198             host, port = self.client_address
199             return socket.getfqdn(host)
201 def usage(message=''):
202     if message:
203         message = _('Error: %(error)s\n\n')%{'error': message}
204     print _('''%(message)sUsage:
205 roundup-server [options] [name=tracker home]*
207 options:
208  -n: sets the host name
209  -p: sets the port to listen on
210  -u: sets the uid to this user after listening on the port
211  -g: sets the gid to this group after listening on the port
212  -l: sets a filename to log to (instead of stdout)
213  -d: sets a filename to write server PID to. This option causes the server 
214      to run in the background. Note: on Windows the PID argument is needed,
215      but ignored. The -l option *must* be specified if this option is.
216  -N: log client machine names in access log instead of IP addresses (much
217      slower)
219 name=tracker home:
220    Sets the tracker home(s) to use. The name is how the tracker is
221    identified in the URL (it's the first part of the URL path). The
222    tracker home is the directory that was identified when you did
223    "roundup-admin init". You may specify any number of these name=home
224    pairs on the command-line. For convenience, you may edit the
225    TRACKER_HOMES variable in the roundup-server file instead.
226    Make sure the name part doesn't include any url-unsafe characters like 
227    spaces, as these confuse the cookie handling in browsers like IE.
228 ''')%locals()
229     sys.exit(0)
231 def daemonize(pidfile):
232     ''' Turn this process into a daemon.
233         - make sure the sys.std(in|out|err) are completely cut off
234         - make our parent PID 1
236         Write our new PID to the pidfile.
238         From A.M. Kuuchling (possibly originally Greg Ward) with
239         modification from Oren Tirosh, and finally a small mod from me.
240     '''
241     # Fork once
242     if os.fork() != 0:
243         os._exit(0)
245     # Create new session
246     os.setsid()
248     # Second fork to force PPID=1
249     pid = os.fork()
250     if pid:
251         pidfile = open(pidfile, 'w')
252         pidfile.write(str(pid))
253         pidfile.close()
254         os._exit(0)         
256     os.chdir("/")         
257     os.umask(0)
259     # close off sys.std(in|out|err), redirect to devnull so the file
260     # descriptors can't be used again
261     devnull = os.open('/dev/null', 0)
262     os.dup2(devnull, 0)
263     os.dup2(devnull, 1)
264     os.dup2(devnull, 2)
266 def run(port=8080, success_message=None):
267     ''' Script entry point - handle args and figure out what to to.
268     '''
269     # time out after a minute if we can
270     import socket
271     if hasattr(socket, 'setdefaulttimeout'):
272         socket.setdefaulttimeout(60)
274     hostname = ''
275     pidfile = None
276     logfile = None
277     try:
278         # handle the command-line args
279         try:
280             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:hN')
281         except getopt.GetoptError, e:
282             usage(str(e))
284         user = ROUNDUP_USER
285         group = None
286         for (opt, arg) in optlist:
287             if opt == '-n': hostname = arg
288             elif opt == '-p': port = int(arg)
289             elif opt == '-u': user = arg
290             elif opt == '-g': group = arg
291             elif opt == '-d': pidfile = os.path.abspath(arg)
292             elif opt == '-l': logfile = os.path.abspath(arg)
293             elif opt == '-h': usage()
294             elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
296         if pidfile and not logfile:
297             raise ValueError, _("logfile *must* be specified if pidfile is")
298   
299         # obtain server before changing user id - allows to use port <
300         # 1024 if started as root
301         address = (hostname, port)
302         httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
304         if group is not None and hasattr(os, 'getgid'):
305             # if root, setgid to the running user
306             if not os.getgid() and user is not None:
307                 try:
308                     import pwd
309                 except ImportError:
310                     raise ValueError, _("Can't change groups - no pwd module")
311                 try:
312                     gid = pwd.getpwnam(user)[3]
313                 except KeyError:
314                     raise ValueError,_("Group %(group)s doesn't exist")%locals()
315                 os.setgid(gid)
316             elif os.getgid() and user is not None:
317                 print _('WARNING: ignoring "-g" argument, not root')
319         if hasattr(os, 'getuid'):
320             # if root, setuid to the running user
321             if not os.getuid() and user is not None:
322                 try:
323                     import pwd
324                 except ImportError:
325                     raise ValueError, _("Can't change users - no pwd module")
326                 try:
327                     uid = pwd.getpwnam(user)[2]
328                 except KeyError:
329                     raise ValueError, _("User %(user)s doesn't exist")%locals()
330                 os.setuid(uid)
331             elif os.getuid() and user is not None:
332                 print _('WARNING: ignoring "-u" argument, not root')
334             # People can remove this check if they're really determined
335             if not os.getuid() and user is None:
336                 raise ValueError, _("Can't run as root!")
338         # handle tracker specs
339         if args:
340             d = {}
341             for arg in args:
342                 try:
343                     name, home = arg.split('=')
344                 except ValueError:
345                     raise ValueError, _("Instances must be name=home")
346                 d[name] = home
347             RoundupRequestHandler.TRACKER_HOMES = d
348     except SystemExit:
349         raise
350     except:
351         exc_type, exc_value = sys.exc_info()[:2]
352         usage('%s: %s'%(exc_type, exc_value))
354     # we don't want the cgi module interpreting the command-line args ;)
355     sys.argv = sys.argv[:1]
357     if pidfile:
358         if not hasattr(os, 'fork'):
359             print "Sorry, you can't run the server as a daemon on this" \
360                 'Operating System'
361             sys.exit(0)
362         else:
363             daemonize(pidfile)
365     # redirect stdout/stderr to our logfile
366     if logfile:
367         # appending, unbuffered
368         sys.stdout = sys.stderr = open(logfile, 'a', 0)
370     if success_message:
371         print success_message
372     else:
373         print _('Roundup server started on %(address)s')%locals()
375     try:
376         httpd.serve_forever()
377     except KeyboardInterrupt:
378         print 'Keyboard Interrupt: exiting'
380 if __name__ == '__main__':
381     run()
383 # vim: set filetype=python ts=4 sw=4 et si