1 #!/usr/bin/python
2 """ HTTP Server that serves roundup.
4 Stolen from CGIHTTPServer
6 $Id: roundup-server,v 1.6 2001-07-29 07:01:39 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()
262 #
263 # $Log: not supported by cvs2svn $
264 # Revision 1.5 2001/07/24 01:07:59 richard
265 # Added command-line arg handling to roundup-server so it's more useful
266 # out-of-the-box.
267 #
268 # Revision 1.4 2001/07/23 10:31:45 richard
269 # disabled the reloading until it can be done properly
270 #
271 # Revision 1.3 2001/07/23 08:53:44 richard
272 # Fixed the ROUNDUPS decl in roundup-server
273 # Move the installation notes to INSTALL
274 #
275 # Revision 1.2 2001/07/23 04:05:05 anthonybaxter
276 # actually quit if python version wrong
277 #
278 # Revision 1.1 2001/07/23 03:46:48 richard
279 # moving the bin files to facilitate out-of-the-boxness
280 #
281 # Revision 1.1 2001/07/22 11:15:45 richard
282 # More Grande Splite stuff
283 #
284 #
285 # vim: set filetype=python ts=4 sw=4 et si