Code

CGI interfaces now spit up a top-level index of all the instances they can
[roundup.git] / roundup-server
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
8 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
9 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
10 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
11 # POSSIBILITY OF SUCH DAMAGE.
12 #
13 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
15 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18
19 """ HTTP Server that serves roundup.
21 Based on CGIHTTPServer in the Python library.
23 $Id: roundup-server,v 1.12 2001-09-29 13:27:00 richard Exp $
25 """
26 import sys
27 if int(sys.version[0]) < 2:
28     print "Content-Type: text/plain\n"
29     print "Roundup requires Python 2.0 or newer."
30     sys.exit(0)
32 import os, urllib, StringIO, traceback, cgi, binascii, string, getopt, imp
33 import BaseHTTPServer
34 import SimpleHTTPServer
36 # Roundup modules of use here
37 from roundup import cgitb, cgi_client
38 import roundup.instance
40 #
41 ##  Configuration
42 #
44 # This indicates where the Roundup instance lives
45 ROUNDUP_INSTANCE_HOMES = {
46     'bar': '/tmp/bar',
47 }
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 #
63 class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
64     ROUNDUP_INSTANCE_HOMES = ROUNDUP_INSTANCE_HOMES
65     def send_head(self):
66         """Version of send_head that support CGI scripts"""
67         # TODO: actually do the HEAD ...
68         return self.run_cgi()
70     def run_cgi(self):
71         """ Execute the CGI command. Wrap an innner call in an error
72             handler so all errors can be caught.
73         """
74         save_stdin = sys.stdin
75         sys.stdin = self.rfile
76         try:
77             self.inner_run_cgi()
78         except cgi_client.Unauthorised:
79             self.wfile.write('Content-Type: text/html\n')
80             self.wfile.write('Status: 403\n')
81             self.wfile.write('Unauthorised')
82         except:
83             try:
84                 reload(cgitb)
85                 self.wfile.write("Content-Type: text/html\n\n")
86                 self.wfile.write(cgitb.breaker())
87                 self.wfile.write(cgitb.html())
88             except:
89                 self.wfile.write("Content-Type: text/html\n\n")
90                 self.wfile.write("<pre>")
91                 s = StringIO.StringIO()
92                 traceback.print_exc(None, s)
93                 self.wfile.write(cgi.escape(s.getvalue()))
94                 self.wfile.write("</pre>\n")
95         sys.stdin = save_stdin
97     def index(self):
98         ''' Print up an index of the available instances
99         '''
100         w = self.wfile.write
101         w("Content-Type: text/html\n\n")
102         w('<html><head><title>Roundup instances index</title><head>\n')
103         w('<body><h1>Roundup instances index</h1><ol>\n')
104         for instance in self.ROUNDUP_INSTANCE_HOMES.keys():
105             w('<li><a href="%s/index">%s</a>\n'%(urllib.quote(instance),
106                 instance))
107         w('</ol></body></html>')
109     def inner_run_cgi(self):
110         ''' This is the inner part of the CGI handling
111         '''
113         rest = self.path
114         i = rest.rfind('?')
115         if i >= 0:
116             rest, query = rest[:i], rest[i+1:]
117         else:
118             query = ''
120         # figure the instance
121         if rest == '/':
122             return self.index()
123         l_path = string.split(rest, '/')
124         instance = urllib.unquote(l_path[1])
125         if self.ROUNDUP_INSTANCE_HOMES.has_key(instance):
126             instance_home = self.ROUNDUP_INSTANCE_HOMES[instance]
127             instance = roundup.instance.open(instance_home)
128         else:
129             return self.index()
131         # figure out what the rest of the path is
132         if len(l_path) > 2:
133             rest = '/'.join(l_path[2:])
134         else:
135             rest = '/'
137         # Set up the CGI environment
138         env = {}
139         env['REQUEST_METHOD'] = self.command
140         env['PATH_INFO'] = urllib.unquote(rest)
141         if query:
142             env['QUERY_STRING'] = query
143         host = self.address_string()
144         if self.headers.typeheader is None:
145             env['CONTENT_TYPE'] = self.headers.type
146         else:
147             env['CONTENT_TYPE'] = self.headers.typeheader
148         length = self.headers.getheader('content-length')
149         if length:
150             env['CONTENT_LENGTH'] = length
151         co = filter(None, self.headers.getheaders('cookie'))
152         if co:
153             env['HTTP_COOKIE'] = ', '.join(co)
154         env['SCRIPT_NAME'] = ''
155         env['SERVER_NAME'] = self.server.server_name
156         env['SERVER_PORT'] = str(self.server.server_port)
158         decoded_query = query.replace('+', ' ')
160         # if root, setuid to nobody
161         # TODO why isn't this done much earlier? - say, in main()?
162         if not os.getuid():
163             nobody = nobody_uid()
164             os.setuid(nobody)
166         # reload all modules
167         # TODO check for file timestamp changes and dependencies
168         #reload(date)
169         #reload(hyperdb)
170         #reload(roundupdb)
171         #reload(htmltemplate)
172         #reload(cgi_client)
173         #sys.path.insert(0, module_path)
174         #try:
175         #    reload(instance)
176         #finally:
177         #    del sys.path[0]
179         # initialise the roundupdb, check for auth
180         db = instance.open('admin')
181         message = 'Unauthorised'
182         auth = self.headers.getheader('authorization')
183         if auth:
184             l = binascii.a2b_base64(auth.split(' ')[1]).split(':')
185             user = l[0]
186             password = None
187             if len(l) > 1:
188                 password = l[1]
189             try:
190                 uid = db.user.lookup(user)
191             except KeyError:
192                 auth = None
193                 message = 'Username not recognised'
194             else:
195                 if password != db.user.get(uid, 'password'):
196                     message = 'Incorrect password'
197                     auth = None
198         db.close()
199         del db
200         if not auth:
201             self.send_response(401)
202             self.send_header('Content-Type', 'text/html')
203             self.send_header('WWW-Authenticate', 'basic realm="Roundup"')
204             self.end_headers()
205             self.wfile.write(message)
206             return
208         self.send_response(200, "Script output follows")
210         # do the roundup thang
211         db = instance.open(user)
212         client = instance.Client(self.wfile, db, env, user)
213         client.main()
214     do_POST = run_cgi
216 nobody = None
218 def nobody_uid():
219     """Internal routine to get nobody's uid"""
220     global nobody
221     if nobody:
222         return nobody
223     try:
224         import pwd
225     except ImportError:
226         return -1
227     try:
228         nobody = pwd.getpwnam('nobody')[2]
229     except KeyError:
230         nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
231     return nobody
233 def usage(message=''):
234     if message: message = 'Error: %s\n'%message
235     print '''%sUsage:
236 roundup-server [-n hostname] [-p port] [name=instance home]*
238  -n: sets the host name
239  -p: sets the port to listen on
241  name=instance home
242    Sets the instance home(s) to use. The name is how the instance is
243    identified in the URL (it's the first part of the URL path). The
244    instance home is the directory that was identified when you did
245    "roundup-admin init". You may specify any number of these name=home
246    pairs on the command-line. For convenience, you may edit the
247    ROUNDUP_INSTANCE_HOMES variable in the roundup-server file instead.
248 '''%message
249     sys.exit(0)
251 def main():
252     hostname = ''
253     port = 8080
254     try:
255         # handle the command-line args
256         optlist, args = getopt.getopt(sys.argv[1:], 'n:p:')
257         for (opt, arg) in optlist:
258             if opt == '-n': hostname = arg
259             elif opt == '-p': port = int(arg)
260             elif opt == '-h': usage()
262         # handle instance specs
263         if args:
264             d = {}
265             for arg in args:
266                 name, home = string.split(arg, '=')
267                 d[name] = home
268             RoundupRequestHandler.ROUNDUP_INSTANCE_HOMES = d
269     except:
270         type, value = sys.exc_info()[:2]
271         usage('%s: %s'%(type, value))
273     # we don't want the cgi module interpreting the command-line args ;)
274     sys.argv = sys.argv[:1]
275     address = (hostname, port)
276     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
277     print 'Roundup server started on', address
278     httpd.serve_forever()
280 if __name__ == '__main__':
281     main()
284 # $Log: not supported by cvs2svn $
285 # Revision 1.11  2001/08/07 00:24:42  richard
286 # stupid typo
288 # Revision 1.10  2001/08/07 00:15:51  richard
289 # Added the copyright/license notice to (nearly) all files at request of
290 # Bizar Software.
292 # Revision 1.9  2001/08/05 07:44:36  richard
293 # Instances are now opened by a special function that generates a unique
294 # module name for the instances on import time.
296 # Revision 1.8  2001/08/03 01:28:33  richard
297 # Used the much nicer load_package, pointed out by Steve Majewski.
299 # Revision 1.7  2001/08/03 00:59:34  richard
300 # Instance import now imports the instance using imp.load_module so that
301 # we can have instance homes of "roundup" or other existing python package
302 # names.
304 # Revision 1.6  2001/07/29 07:01:39  richard
305 # Added vim command to all source so that we don't get no steenkin' tabs :)
307 # Revision 1.5  2001/07/24 01:07:59  richard
308 # Added command-line arg handling to roundup-server so it's more useful
309 # out-of-the-box.
311 # Revision 1.4  2001/07/23 10:31:45  richard
312 # disabled the reloading until it can be done properly
314 # Revision 1.3  2001/07/23 08:53:44  richard
315 # Fixed the ROUNDUPS decl in roundup-server
316 # Move the installation notes to INSTALL
318 # Revision 1.2  2001/07/23 04:05:05  anthonybaxter
319 # actually quit if python version wrong
321 # Revision 1.1  2001/07/23 03:46:48  richard
322 # moving the bin files to facilitate out-of-the-boxness
324 # Revision 1.1  2001/07/22 11:15:45  richard
325 # More Grande Splite stuff
328 # vim: set filetype=python ts=4 sw=4 et si