Code

Fixed sf.net bug #447671 - typo
[roundup.git] / roundup-server
1 #!/usr/bin/python
2 """ HTTP Server that serves roundup.
4 Stolen from CGIHTTPServer
6 $Id: roundup-server,v 1.8 2001-08-03 01:28:33 richard Exp $
8 """
9 import sys
10 if int(sys.version[0]) < 2:
11     print "Content-Type: text/plain\n"
12     print "Roundup requires Python 2.0 or newer."
13     sys.exit(0)
15 __version__ = "0.1"
17 __all__ = ["RoundupRequestHandler"]
19 import os, urllib, StringIO, traceback, cgi, binascii, string, getopt, imp
20 import BaseHTTPServer
21 import SimpleHTTPServer
23 # Roundup modules of use here
24 from roundup import cgitb, cgi_client
26 #
27 ##  Configuration
28 #
30 # This indicates where the Roundup instance lives
31 ROUNDUP_INSTANCE_HOMES = {
32     'bar': '/tmp/bar',
33 }
35 # Where to log debugging information to. Use an instance of DevNull if you
36 # don't want to log anywhere.
37 # TODO: actually use this stuff
38 #class DevNull:
39 #    def write(self, info):
40 #        pass
41 #LOG = open('/var/log/roundup.cgi.log', 'a')
42 #LOG = DevNull()
44 #
45 ##  end configuration
46 #
49 class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
50     ROUNDUP_INSTANCE_HOMES = ROUNDUP_INSTANCE_HOMES
51     def send_head(self):
52         """Version of send_head that support CGI scripts"""
53         # TODO: actually do the HEAD ...
54         return self.run_cgi()
56     def run_cgi(self):
57         """ Execute the CGI command. Wrap an innner call in an error
58             handler so all errors can be caught.
59         """
60         save_stdin = sys.stdin
61         sys.stdin = self.rfile
62         try:
63             self.inner_run_cgi()
64         except cgi_client.Unauthorised:
65             self.wfile.write('Content-Type: text/html\n')
66             self.wfile.write('Status: 403\n')
67             self.wfile.write('Unauthorised')
68         except:
69             try:
70                 reload(cgitb)
71                 self.wfile.write("Content-Type: text/html\n\n")
72                 self.wfile.write(cgitb.breaker())
73                 self.wfile.write(cgitb.html())
74             except:
75                 self.wfile.write("Content-Type: text/html\n\n")
76                 self.wfile.write("<pre>")
77                 s = StringIO.StringIO()
78                 traceback.print_exc(None, s)
79                 self.wfile.write(cgi.escape(s.getvalue()))
80                 self.wfile.write("</pre>\n")
81         sys.stdin = save_stdin
83     def inner_run_cgi(self):
84         ''' This is the inner part of the CGI handling
85         '''
87         rest = self.path
88         i = rest.rfind('?')
89         if i >= 0:
90             rest, query = rest[:i], rest[i+1:]
91         else:
92             query = ''
94         # figure the instance
95         if rest == '/':
96             raise ValueError, 'No instance specified'
97         l_path = string.split(rest, '/')
98         instance = urllib.unquote(l_path[1])
99         if self.ROUNDUP_INSTANCE_HOMES.has_key(instance):
100             instance_home = self.ROUNDUP_INSTANCE_HOMES[instance]
101             instance = imp.load_package('instance', instance_home)
102         else:
103             raise ValueError, 'No such instance "%s"'%instance
105         # figure out what the rest of the path is
106         if len(l_path) > 2:
107             rest = '/'.join(l_path[2:])
108         else:
109             rest = '/'
111         # Set up the CGI environment
112         env = {}
113         env['REQUEST_METHOD'] = self.command
114         env['PATH_INFO'] = urllib.unquote(rest)
115         if query:
116             env['QUERY_STRING'] = query
117         host = self.address_string()
118         if self.headers.typeheader is None:
119             env['CONTENT_TYPE'] = self.headers.type
120         else:
121             env['CONTENT_TYPE'] = self.headers.typeheader
122         length = self.headers.getheader('content-length')
123         if length:
124             env['CONTENT_LENGTH'] = length
125         co = filter(None, self.headers.getheaders('cookie'))
126         if co:
127             env['HTTP_COOKIE'] = ', '.join(co)
128         env['SCRIPT_NAME'] = ''
129         env['SERVER_NAME'] = self.server.server_name
130         env['SERVER_PORT'] = str(self.server.server_port)
132         decoded_query = query.replace('+', ' ')
134         # if root, setuid to nobody
135         # TODO why isn't this done much earlier? - say, in main()?
136         if not os.getuid():
137             nobody = nobody_uid()
138             os.setuid(nobody)
140         # reload all modules
141         # TODO check for file timestamp changes and dependencies
142         #reload(date)
143         #reload(hyperdb)
144         #reload(roundupdb)
145         #reload(htmltemplate)
146         #reload(cgi_client)
147         #sys.path.insert(0, module_path)
148         #try:
149         #    reload(instance)
150         #finally:
151         #    del sys.path[0]
153         # initialise the roundupdb, check for auth
154         db = instance.open('admin')
155         message = 'Unauthorised'
156         auth = self.headers.getheader('authorization')
157         if auth:
158             l = binascii.a2b_base64(auth.split(' ')[1]).split(':')
159             user = l[0]
160             password = None
161             if len(l) > 1:
162                 password = l[1]
163             try:
164                 uid = db.user.lookup(user)
165             except KeyError:
166                 auth = None
167                 message = 'Username not recognised'
168             else:
169                 if password != db.user.get(uid, 'password'):
170                     message = 'Incorrect password'
171                     auth = None
172         db.close()
173         del db
174         if not auth:
175             self.send_response(401)
176             self.send_header('Content-Type', 'text/html')
177             self.send_header('WWW-Authenticate', 'basic realm="Roundup"')
178             self.end_headers()
179             self.wfile.write(message)
180             return
182         self.send_response(200, "Script output follows")
184         # do the roundup thang
185         db = instance.open(user)
186         client = instance.Client(self.wfile, db, env, user)
187         client.main()
188     do_POST = run_cgi
190 nobody = None
192 def nobody_uid():
193     """Internal routine to get nobody's uid"""
194     global nobody
195     if nobody:
196         return nobody
197     try:
198         import pwd
199     except ImportError:
200         return -1
201     try:
202         nobody = pwd.getpwnam('nobody')[2]
203     except KeyError:
204         nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
205     return nobody
207 def usage(message=''):
208     if message: message = 'Error: %s\n'%message
209     print '''%sUsage:
210 roundup-server [-n hostname] [-p port] [name=instance home]*
212  -n: sets the host name
213  -p: sets the port to listen on
215  name=instance home
216    Sets the instance home(s) to use. The name is how the instance is
217    identified in the URL (it's the first part of the URL path). The
218    instance home is the directory that was identified when you did
219    "roundup-admin init". You may specify any number of these name=home
220    pairs on the command-line. For convenience, you may edit the
221    ROUNDUP_INSTANCE_HOMES variable in the roundup-server file instead.
222 '''%message
223     sys.exit(0)
225 def main():
226     hostname = ''
227     port = 8080
228     try:
229         # handle the command-line args
230         optlist, args = getopt.getopt(sys.argv[1:], 'n:p:')
231         for (opt, arg) in optlist:
232             if opt == '-n': hostname = arg
233             elif opt == '-p': port = int(arg)
234             elif opt == '-h': usage()
236         # handle instance specs
237         if args:
238             d = {}
239             for arg in args:
240                 name, home = string.split(arg, '=')
241                 d[name] = home
242             RoundupRequestHandler.ROUNDUP_INSTANCE_HOMES = d
243     except:
244         type, value = sys.exc_info()[:2]
245         usage('%s: %s'%(type, value))
247     # we don't want the cgi module interpreting the command-line args ;)
248     sys.argv = sys.argv[:1]
249     address = (hostname, port)
250     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
251     print 'Roundup server started on', address
252     httpd.serve_forever()
254 if __name__ == '__main__':
255     main()
258 # $Log: not supported by cvs2svn $
259 # Revision 1.7  2001/08/03 00:59:34  richard
260 # Instance import now imports the instance using imp.load_module so that
261 # we can have instance homes of "roundup" or other existing python package
262 # names.
264 # Revision 1.6  2001/07/29 07:01:39  richard
265 # Added vim command to all source so that we don't get no steenkin' tabs :)
267 # Revision 1.5  2001/07/24 01:07:59  richard
268 # Added command-line arg handling to roundup-server so it's more useful
269 # out-of-the-box.
271 # Revision 1.4  2001/07/23 10:31:45  richard
272 # disabled the reloading until it can be done properly
274 # Revision 1.3  2001/07/23 08:53:44  richard
275 # Fixed the ROUNDUPS decl in roundup-server
276 # Move the installation notes to INSTALL
278 # Revision 1.2  2001/07/23 04:05:05  anthonybaxter
279 # actually quit if python version wrong
281 # Revision 1.1  2001/07/23 03:46:48  richard
282 # moving the bin files to facilitate out-of-the-boxness
284 # Revision 1.1  2001/07/22 11:15:45  richard
285 # More Grande Splite stuff
288 # vim: set filetype=python ts=4 sw=4 et si