Code

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