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()
283 #
284 # $Log: not supported by cvs2svn $
285 # Revision 1.11 2001/08/07 00:24:42 richard
286 # stupid typo
287 #
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.
291 #
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.
295 #
296 # Revision 1.8 2001/08/03 01:28:33 richard
297 # Used the much nicer load_package, pointed out by Steve Majewski.
298 #
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.
303 #
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 :)
306 #
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.
310 #
311 # Revision 1.4 2001/07/23 10:31:45 richard
312 # disabled the reloading until it can be done properly
313 #
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
317 #
318 # Revision 1.2 2001/07/23 04:05:05 anthonybaxter
319 # actually quit if python version wrong
320 #
321 # Revision 1.1 2001/07/23 03:46:48 richard
322 # moving the bin files to facilitate out-of-the-boxness
323 #
324 # Revision 1.1 2001/07/22 11:15:45 richard
325 # More Grande Splite stuff
326 #
327 #
328 # vim: set filetype=python ts=4 sw=4 et si