Code

61273e54bd4933d447293d4a48c6313b1b02728c
[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 THE 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.10 2001-08-07 00:15:51 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 inner_run_cgi(self):
98         ''' This is the inner part of the CGI handling
99         '''
101         rest = self.path
102         i = rest.rfind('?')
103         if i >= 0:
104             rest, query = rest[:i], rest[i+1:]
105         else:
106             query = ''
108         # figure the instance
109         if rest == '/':
110             raise ValueError, 'No instance specified'
111         l_path = string.split(rest, '/')
112         instance = urllib.unquote(l_path[1])
113         if self.ROUNDUP_INSTANCE_HOMES.has_key(instance):
114             instance_home = self.ROUNDUP_INSTANCE_HOMES[instance]
115             instance = roundup.instance.open(instance_home)
116         else:
117             raise ValueError, 'No such instance "%s"'%instance
119         # figure out what the rest of the path is
120         if len(l_path) > 2:
121             rest = '/'.join(l_path[2:])
122         else:
123             rest = '/'
125         # Set up the CGI environment
126         env = {}
127         env['REQUEST_METHOD'] = self.command
128         env['PATH_INFO'] = urllib.unquote(rest)
129         if query:
130             env['QUERY_STRING'] = query
131         host = self.address_string()
132         if self.headers.typeheader is None:
133             env['CONTENT_TYPE'] = self.headers.type
134         else:
135             env['CONTENT_TYPE'] = self.headers.typeheader
136         length = self.headers.getheader('content-length')
137         if length:
138             env['CONTENT_LENGTH'] = length
139         co = filter(None, self.headers.getheaders('cookie'))
140         if co:
141             env['HTTP_COOKIE'] = ', '.join(co)
142         env['SCRIPT_NAME'] = ''
143         env['SERVER_NAME'] = self.server.server_name
144         env['SERVER_PORT'] = str(self.server.server_port)
146         decoded_query = query.replace('+', ' ')
148         # if root, setuid to nobody
149         # TODO why isn't this done much earlier? - say, in main()?
150         if not os.getuid():
151             nobody = nobody_uid()
152             os.setuid(nobody)
154         # reload all modules
155         # TODO check for file timestamp changes and dependencies
156         #reload(date)
157         #reload(hyperdb)
158         #reload(roundupdb)
159         #reload(htmltemplate)
160         #reload(cgi_client)
161         #sys.path.insert(0, module_path)
162         #try:
163         #    reload(instance)
164         #finally:
165         #    del sys.path[0]
167         # initialise the roundupdb, check for auth
168         db = instance.open('admin')
169         message = 'Unauthorised'
170         auth = self.headers.getheader('authorization')
171         if auth:
172             l = binascii.a2b_base64(auth.split(' ')[1]).split(':')
173             user = l[0]
174             password = None
175             if len(l) > 1:
176                 password = l[1]
177             try:
178                 uid = db.user.lookup(user)
179             except KeyError:
180                 auth = None
181                 message = 'Username not recognised'
182             else:
183                 if password != db.user.get(uid, 'password'):
184                     message = 'Incorrect password'
185                     auth = None
186         db.close()
187         del db
188         if not auth:
189             self.send_response(401)
190             self.send_header('Content-Type', 'text/html')
191             self.send_header('WWW-Authenticate', 'basic realm="Roundup"')
192             self.end_headers()
193             self.wfile.write(message)
194             return
196         self.send_response(200, "Script output follows")
198         # do the roundup thang
199         db = instance.open(user)
200         client = instance.Client(self.wfile, db, env, user)
201         client.main()
202     do_POST = run_cgi
204 nobody = None
206 def nobody_uid():
207     """Internal routine to get nobody's uid"""
208     global nobody
209     if nobody:
210         return nobody
211     try:
212         import pwd
213     except ImportError:
214         return -1
215     try:
216         nobody = pwd.getpwnam('nobody')[2]
217     except KeyError:
218         nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
219     return nobody
221 def usage(message=''):
222     if message: message = 'Error: %s\n'%message
223     print '''%sUsage:
224 roundup-server [-n hostname] [-p port] [name=instance home]*
226  -n: sets the host name
227  -p: sets the port to listen on
229  name=instance home
230    Sets the instance home(s) to use. The name is how the instance is
231    identified in the URL (it's the first part of the URL path). The
232    instance home is the directory that was identified when you did
233    "roundup-admin init". You may specify any number of these name=home
234    pairs on the command-line. For convenience, you may edit the
235    ROUNDUP_INSTANCE_HOMES variable in the roundup-server file instead.
236 '''%message
237     sys.exit(0)
239 def main():
240     hostname = ''
241     port = 8080
242     try:
243         # handle the command-line args
244         optlist, args = getopt.getopt(sys.argv[1:], 'n:p:')
245         for (opt, arg) in optlist:
246             if opt == '-n': hostname = arg
247             elif opt == '-p': port = int(arg)
248             elif opt == '-h': usage()
250         # handle instance specs
251         if args:
252             d = {}
253             for arg in args:
254                 name, home = string.split(arg, '=')
255                 d[name] = home
256             RoundupRequestHandler.ROUNDUP_INSTANCE_HOMES = d
257     except:
258         type, value = sys.exc_info()[:2]
259         usage('%s: %s'%(type, value))
261     # we don't want the cgi module interpreting the command-line args ;)
262     sys.argv = sys.argv[:1]
263     address = (hostname, port)
264     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
265     print 'Roundup server started on', address
266     httpd.serve_forever()
268 if __name__ == '__main__':
269     main()
272 # $Log: not supported by cvs2svn $
273 # Revision 1.9  2001/08/05 07:44:36  richard
274 # Instances are now opened by a special function that generates a unique
275 # module name for the instances on import time.
277 # Revision 1.8  2001/08/03 01:28:33  richard
278 # Used the much nicer load_package, pointed out by Steve Majewski.
280 # Revision 1.7  2001/08/03 00:59:34  richard
281 # Instance import now imports the instance using imp.load_module so that
282 # we can have instance homes of "roundup" or other existing python package
283 # names.
285 # Revision 1.6  2001/07/29 07:01:39  richard
286 # Added vim command to all source so that we don't get no steenkin' tabs :)
288 # Revision 1.5  2001/07/24 01:07:59  richard
289 # Added command-line arg handling to roundup-server so it's more useful
290 # out-of-the-box.
292 # Revision 1.4  2001/07/23 10:31:45  richard
293 # disabled the reloading until it can be done properly
295 # Revision 1.3  2001/07/23 08:53:44  richard
296 # Fixed the ROUNDUPS decl in roundup-server
297 # Move the installation notes to INSTALL
299 # Revision 1.2  2001/07/23 04:05:05  anthonybaxter
300 # actually quit if python version wrong
302 # Revision 1.1  2001/07/23 03:46:48  richard
303 # moving the bin files to facilitate out-of-the-boxness
305 # Revision 1.1  2001/07/22 11:15:45  richard
306 # More Grande Splite stuff
309 # vim: set filetype=python ts=4 sw=4 et si