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()
257 #
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.
263 #
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 :)
266 #
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.
270 #
271 # Revision 1.4 2001/07/23 10:31:45 richard
272 # disabled the reloading until it can be done properly
273 #
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
277 #
278 # Revision 1.2 2001/07/23 04:05:05 anthonybaxter
279 # actually quit if python version wrong
280 #
281 # Revision 1.1 2001/07/23 03:46:48 richard
282 # moving the bin files to facilitate out-of-the-boxness
283 #
284 # Revision 1.1 2001/07/22 11:15:45 richard
285 # More Grande Splite stuff
286 #
287 #
288 # vim: set filetype=python ts=4 sw=4 et si