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.11 2001-08-07 00:24:42 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()
271 #
272 # $Log: not supported by cvs2svn $
273 # Revision 1.10 2001/08/07 00:15:51 richard
274 # Added the copyright/license notice to (nearly) all files at request of
275 # Bizar Software.
276 #
277 # Revision 1.9 2001/08/05 07:44:36 richard
278 # Instances are now opened by a special function that generates a unique
279 # module name for the instances on import time.
280 #
281 # Revision 1.8 2001/08/03 01:28:33 richard
282 # Used the much nicer load_package, pointed out by Steve Majewski.
283 #
284 # Revision 1.7 2001/08/03 00:59:34 richard
285 # Instance import now imports the instance using imp.load_module so that
286 # we can have instance homes of "roundup" or other existing python package
287 # names.
288 #
289 # Revision 1.6 2001/07/29 07:01:39 richard
290 # Added vim command to all source so that we don't get no steenkin' tabs :)
291 #
292 # Revision 1.5 2001/07/24 01:07:59 richard
293 # Added command-line arg handling to roundup-server so it's more useful
294 # out-of-the-box.
295 #
296 # Revision 1.4 2001/07/23 10:31:45 richard
297 # disabled the reloading until it can be done properly
298 #
299 # Revision 1.3 2001/07/23 08:53:44 richard
300 # Fixed the ROUNDUPS decl in roundup-server
301 # Move the installation notes to INSTALL
302 #
303 # Revision 1.2 2001/07/23 04:05:05 anthonybaxter
304 # actually quit if python version wrong
305 #
306 # Revision 1.1 2001/07/23 03:46:48 richard
307 # moving the bin files to facilitate out-of-the-boxness
308 #
309 # Revision 1.1 2001/07/22 11:15:45 richard
310 # More Grande Splite stuff
311 #
312 #
313 # vim: set filetype=python ts=4 sw=4 et si