Code

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