Code

- fixed filter() with no sort/group (sf bug 618614)
[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.13 2002-10-07 00:52:51 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 instances
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 instances index</title></head>\n'))
106         w(_('<body><h1>Roundup instances index</h1><ol>\n'))
107         for instance in self.TRACKER_HOMES.keys():
108             w(_('<li><a href="%(instance_url)s/index">%(instance_name)s</a>\n')%{
109                 'instance_url': urllib.quote(instance),
110                 'instance_name': cgi.escape(instance)})
111         w(_('</ol></body></html>'))
113     def inner_run_cgi(self):
114         ''' This is the inner part of the CGI handling
115         '''
117         rest = self.path
118         i = rest.rfind('?')
119         if i >= 0:
120             rest, query = rest[:i], rest[i+1:]
121         else:
122             query = ''
124         # figure the instance
125         if rest == '/':
126             return self.index()
127         l_path = rest.split('/')
128         instance_name = urllib.unquote(l_path[1])
129         if self.TRACKER_HOMES.has_key(instance_name):
130             instance_home = self.TRACKER_HOMES[instance_name]
131             instance = roundup.instance.open(instance_home)
132         else:
133             raise client.NotFound
135         # figure out what the rest of the path is
136         if len(l_path) > 2:
137             rest = '/'.join(l_path[2:])
138         else:
139             rest = '/'
141         # Set up the CGI environment
142         env = {}
143         env['TRACKER_NAME'] = instance_name
144         env['REQUEST_METHOD'] = self.command
145         env['PATH_INFO'] = urllib.unquote(rest)
146         if query:
147             env['QUERY_STRING'] = query
148         host = self.address_string()
149         if self.headers.typeheader is None:
150             env['CONTENT_TYPE'] = self.headers.type
151         else:
152             env['CONTENT_TYPE'] = self.headers.typeheader
153         length = self.headers.getheader('content-length')
154         if length:
155             env['CONTENT_LENGTH'] = length
156         co = filter(None, self.headers.getheaders('cookie'))
157         if co:
158             env['HTTP_COOKIE'] = ', '.join(co)
159         env['SCRIPT_NAME'] = ''
160         env['SERVER_NAME'] = self.server.server_name
161         env['SERVER_PORT'] = str(self.server.server_port)
162         env['HTTP_HOST'] = self.headers['host']
164         decoded_query = query.replace('+', ' ')
166         # do the roundup thang
167         c = instance.Client(instance, self, env)
168         c.main()
170 def usage(message=''):
171     if message:
172         message = _('Error: %(error)s\n\n')%{'error': message}
173     print _('''%(message)sUsage:
174 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=instance home]*
176  -n: sets the host name
177  -p: sets the port to listen on
178  -l: sets a filename to log to (instead of stdout)
179  -d: daemonize, and write the server's PID to the nominated file
181  name=instance home
182    Sets the instance home(s) to use. The name is how the instance is
183    identified in the URL (it's the first part of the URL path). The
184    instance home is the directory that was identified when you did
185    "roundup-admin init". You may specify any number of these name=home
186    pairs on the command-line. For convenience, you may edit the
187    TRACKER_HOMES variable in the roundup-server file instead.
188    Make sure the name part doesn't include any url-unsafe characters like 
189    spaces, as these confuse the cookie handling in browsers like IE.
190 ''')%locals()
191     sys.exit(0)
193 def daemonize(pidfile):
194     ''' Turn this process into a daemon.
195         - make sure the sys.std(in|out|err) are completely cut off
196         - make our parent PID 1
198         Write our new PID to the pidfile.
200         From A.M. Kuuchling (possibly originally Greg Ward) with
201         modification from Oren Tirosh, and finally a small mod from me.
202     '''
203     # Fork once
204     if os.fork() != 0:
205         os._exit(0)
207     # Create new session
208     os.setsid()
210     # Second fork to force PPID=1
211     pid = os.fork()
212     if pid:
213         pidfile = open(pidfile, 'w')
214         pidfile.write(str(pid))
215         pidfile.close()
216         os._exit(0)         
218     os.chdir("/")         
219     os.umask(0)
221     # close off sys.std(in|out|err), redirect to devnull so the file
222     # descriptors can't be used again
223     devnull = os.open('/dev/null', 0)
224     os.dup2(devnull, 0)
225     os.dup2(devnull, 1)
226     os.dup2(devnull, 2)
228 def abspath(path):
229     ''' Make the given path an absolute path.
231         Code from Zope-Coders posting of 2002-10-06 by GvR.
232     '''
233     if not os.path.isabs(path):
234         path = os.path.join(os.getcwd(), path)
235     return os.path.normpath(path)
237 def run():
238     ''' Script entry point - handle args and figure out what to to.
239     '''
240     hostname = ''
241     port = 8080
242     pidfile = None
243     logfile = None
244     try:
245         # handle the command-line args
246         try:
247             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:')
248         except getopt.GetoptError, e:
249             usage(str(e))
251         user = ROUNDUP_USER
252         for (opt, arg) in optlist:
253             if opt == '-n': hostname = arg
254             elif opt == '-p': port = int(arg)
255             elif opt == '-u': user = arg
256             elif opt == '-d': pidfile = abspath(arg)
257             elif opt == '-l': logfile = abspath(arg)
258             elif opt == '-h': usage()
260         if hasattr(os, 'getuid'):
261             # if root, setuid to the running user
262             if not os.getuid() and user is not None:
263                 try:
264                     import pwd
265                 except ImportError:
266                     raise ValueError, _("Can't change users - no pwd module")
267                 try:
268                     uid = pwd.getpwnam(user)[2]
269                 except KeyError:
270                     raise ValueError, _("User %(user)s doesn't exist")%locals()
271                 os.setuid(uid)
272             elif os.getuid() and user is not None:
273                 print _('WARNING: ignoring "-u" argument, not root')
275             # People can remove this check if they're really determined
276             if not os.getuid() and user is None:
277                 raise ValueError, _("Can't run as root!")
279         # handle instance specs
280         if args:
281             d = {}
282             for arg in args:
283                 try:
284                     name, home = arg.split('=')
285                 except ValueError:
286                     raise ValueError, _("Instances must be name=home")
287                 d[name] = home
288             RoundupRequestHandler.TRACKER_HOMES = d
289     except SystemExit:
290         raise
291     except:
292         exc_type, exc_value = sys.exc_info()[:2]
293         usage('%s: %s'%(exc_type, exc_value))
295     # we don't want the cgi module interpreting the command-line args ;)
296     sys.argv = sys.argv[:1]
297     address = (hostname, port)
299     # fork?
300     if pidfile:
301         daemonize(pidfile)
303     # redirect stdout/stderr to our logfile
304     if logfile:
305         sys.stdout = sys.stderr = open(logfile, 'a')
307     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
308     print _('Roundup server started on %(address)s')%locals()
309     httpd.serve_forever()
311 if __name__ == '__main__':
312     run()
314 # vim: set filetype=python ts=4 sw=4 et si