Code

regenerated on Wed Jul 25 14:20:54 EST 2001
[roundup.git] / roundup-server
1 #!/usr/bin/python
2 """ HTTP Server that serves roundup.
4 Stolen from CGIHTTPServer
6 $Id: roundup-server,v 1.5 2001-07-24 01:07:59 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
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             module_path, instance = os.path.split(instance_home)
102             sys.path.insert(0, module_path)
103             try:
104                 instance = __import__(instance)
105             finally:
106                 del sys.path[0]
107         else:
108             raise ValueError, 'No such instance "%s"'%instance
110         # figure out what the rest of the path is
111         if len(l_path) > 2:
112             rest = '/'.join(l_path[2:])
113         else:
114             rest = '/'
116         # Set up the CGI environment
117         env = {}
118         env['REQUEST_METHOD'] = self.command
119         env['PATH_INFO'] = urllib.unquote(rest)
120         if query:
121             env['QUERY_STRING'] = query
122         host = self.address_string()
123         if self.headers.typeheader is None:
124             env['CONTENT_TYPE'] = self.headers.type
125         else:
126             env['CONTENT_TYPE'] = self.headers.typeheader
127         length = self.headers.getheader('content-length')
128         if length:
129             env['CONTENT_LENGTH'] = length
130         co = filter(None, self.headers.getheaders('cookie'))
131         if co:
132             env['HTTP_COOKIE'] = ', '.join(co)
133         env['SCRIPT_NAME'] = ''
134         env['SERVER_NAME'] = self.server.server_name
135         env['SERVER_PORT'] = str(self.server.server_port)
137         decoded_query = query.replace('+', ' ')
139         # if root, setuid to nobody
140         # TODO why isn't this done much earlier? - say, in main()?
141         if not os.getuid():
142             nobody = nobody_uid()
143             os.setuid(nobody)
145         # reload all modules
146         # TODO check for file timestamp changes and dependencies
147         #reload(date)
148         #reload(hyperdb)
149         #reload(roundupdb)
150         #reload(htmltemplate)
151         #reload(cgi_client)
152         #sys.path.insert(0, module_path)
153         #try:
154         #    reload(instance)
155         #finally:
156         #    del sys.path[0]
158         # initialise the roundupdb, check for auth
159         db = instance.open('admin')
160         message = 'Unauthorised'
161         auth = self.headers.getheader('authorization')
162         if auth:
163             l = binascii.a2b_base64(auth.split(' ')[1]).split(':')
164             user = l[0]
165             password = None
166             if len(l) > 1:
167                 password = l[1]
168             try:
169                 uid = db.user.lookup(user)
170             except KeyError:
171                 auth = None
172                 message = 'Username not recognised'
173             else:
174                 if password != db.user.get(uid, 'password'):
175                     message = 'Incorrect password'
176                     auth = None
177         db.close()
178         del db
179         if not auth:
180             self.send_response(401)
181             self.send_header('Content-Type', 'text/html')
182             self.send_header('WWW-Authenticate', 'basic realm="Roundup"')
183             self.end_headers()
184             self.wfile.write(message)
185             return
187         self.send_response(200, "Script output follows")
189         # do the roundup thang
190         db = instance.open(user)
191         client = instance.Client(self.wfile, db, env, user)
192         client.main()
193     do_POST = run_cgi
195 nobody = None
197 def nobody_uid():
198     """Internal routine to get nobody's uid"""
199     global nobody
200     if nobody:
201         return nobody
202     try:
203         import pwd
204     except ImportError:
205         return -1
206     try:
207         nobody = pwd.getpwnam('nobody')[2]
208     except KeyError:
209         nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
210     return nobody
212 def usage(message=''):
213     if message: message = 'Error: %s\n'%message
214     print '''%sUsage:
215 roundup-server [-n hostname] [-p port] [name=instance home]*
217  -n: sets the host name
218  -p: sets the port to listen on
220  name=instance home
221    Sets the instance home(s) to use. The name is how the instance is
222    identified in the URL (it's the first part of the URL path). The
223    instance home is the directory that was identified when you did
224    "roundup-admin init". You may specify any number of these name=home
225    pairs on the command-line. For convenience, you may edit the
226    ROUNDUP_INSTANCE_HOMES variable in the roundup-server file instead.
227 '''%message
228     sys.exit(0)
230 def main():
231     hostname = ''
232     port = 8080
233     try:
234         # handle the command-line args
235         optlist, args = getopt.getopt(sys.argv[1:], 'n:p:')
236         for (opt, arg) in optlist:
237             if opt == '-n': hostname = arg
238             elif opt == '-p': port = int(arg)
239             elif opt == '-h': usage()
241         # handle instance specs
242         if args:
243             d = {}
244             for arg in args:
245                 name, home = string.split(arg, '=')
246                 d[name] = home
247             RoundupRequestHandler.ROUNDUP_INSTANCE_HOMES = d
248     except:
249         type, value = sys.exc_info()[:2]
250         usage('%s: %s'%(type, value))
252     # we don't want the cgi module interpreting the command-line args ;)
253     sys.argv = sys.argv[:1]
254     address = (hostname, port)
255     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
256     print 'Roundup server started on', address
257     httpd.serve_forever()
259 if __name__ == '__main__':
260     main()
263 # $Log: not supported by cvs2svn $
264 # Revision 1.4  2001/07/23 10:31:45  richard
265 # disabled the reloading until it can be done properly
267 # Revision 1.3  2001/07/23 08:53:44  richard
268 # Fixed the ROUNDUPS decl in roundup-server
269 # Move the installation notes to INSTALL
271 # Revision 1.2  2001/07/23 04:05:05  anthonybaxter
272 # actually quit if python version wrong
274 # Revision 1.1  2001/07/23 03:46:48  richard
275 # moving the bin files to facilitate out-of-the-boxness
277 # Revision 1.1  2001/07/22 11:15:45  richard
278 # More Grande Splite stuff