Code

Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
[roundup.git] / roundup-server
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.16 2001-10-27 00:12:21 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 ROUNDUP_USER = None
52 # Where to log debugging information to. Use an instance of DevNull if you
53 # don't want to log anywhere.
54 # TODO: actually use this stuff
55 #class DevNull:
56 #    def write(self, info):
57 #        pass
58 #LOG = open('/var/log/roundup.cgi.log', 'a')
59 #LOG = DevNull()
61 #
62 ##  end configuration
63 #
66 class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
67     ROUNDUP_INSTANCE_HOMES = ROUNDUP_INSTANCE_HOMES
68     ROUNDUP_USER = ROUNDUP_USER
69     def send_head(self):
70         """Version of send_head that support CGI scripts"""
71         # TODO: actually do the HEAD ...
72         return self.run_cgi()
74     def run_cgi(self):
75         """ Execute the CGI command. Wrap an innner call in an error
76             handler so all errors can be caught.
77         """
78         save_stdin = sys.stdin
79         sys.stdin = self.rfile
80         try:
81             self.inner_run_cgi()
82         except cgi_client.NotFound:
83             self.send_error(404, self.path)
84         except cgi_client.Unauthorised:
85             self.wfile.write('Content-Type: text/html\n')
86             self.wfile.write('Status: 403\n\n')
87             self.wfile.write('You are not authorised to access this URL.')
88         except:
89             try:
90                 reload(cgitb)
91                 self.wfile.write("Content-Type: text/html\n\n")
92                 self.wfile.write(cgitb.breaker())
93                 self.wfile.write(cgitb.html())
94             except:
95                 self.wfile.write("Content-Type: text/html\n\n")
96                 self.wfile.write("<pre>")
97                 s = StringIO.StringIO()
98                 traceback.print_exc(None, s)
99                 self.wfile.write(cgi.escape(s.getvalue()))
100                 self.wfile.write("</pre>\n")
101         sys.stdin = save_stdin
103     def index(self):
104         ''' Print up an index of the available instances
105         '''
106         w = self.wfile.write
107         w("Content-Type: text/html\n\n")
108         w('<html><head><title>Roundup instances index</title><head>\n')
109         w('<body><h1>Roundup instances index</h1><ol>\n')
110         for instance in self.ROUNDUP_INSTANCE_HOMES.keys():
111             w('<li><a href="%s/index">%s</a>\n'%(urllib.quote(instance),
112                 instance))
113         w('</ol></body></html>')
115     def inner_run_cgi(self):
116         ''' This is the inner part of the CGI handling
117         '''
119         rest = self.path
120         i = rest.rfind('?')
121         if i >= 0:
122             rest, query = rest[:i], rest[i+1:]
123         else:
124             query = ''
126         # figure the instance
127         if rest == '/':
128             return self.index()
129         l_path = string.split(rest, '/')
130         instance_name = urllib.unquote(l_path[1])
131         if self.ROUNDUP_INSTANCE_HOMES.has_key(instance_name):
132             instance_home = self.ROUNDUP_INSTANCE_HOMES[instance_name]
133             instance = roundup.instance.open(instance_home)
134         else:
135             raise cgi_client.NotFound
137         # figure out what the rest of the path is
138         if len(l_path) > 2:
139             rest = '/'.join(l_path[2:])
140         else:
141             rest = '/'
143         # Set up the CGI environment
144         env = {}
145         env['INSTANCE_NAME'] = instance_name
146         env['REQUEST_METHOD'] = self.command
147         env['PATH_INFO'] = urllib.unquote(rest)
148         if query:
149             env['QUERY_STRING'] = query
150         host = self.address_string()
151         if self.headers.typeheader is None:
152             env['CONTENT_TYPE'] = self.headers.type
153         else:
154             env['CONTENT_TYPE'] = self.headers.typeheader
155         length = self.headers.getheader('content-length')
156         if length:
157             env['CONTENT_LENGTH'] = length
158         co = filter(None, self.headers.getheaders('cookie'))
159         if co:
160             env['HTTP_COOKIE'] = ', '.join(co)
161         env['SCRIPT_NAME'] = ''
162         env['SERVER_NAME'] = self.server.server_name
163         env['SERVER_PORT'] = str(self.server.server_port)
165         decoded_query = query.replace('+', ' ')
167         # reload all modules
168         # TODO check for file timestamp changes and dependencies
169         #reload(date)
170         #reload(hyperdb)
171         #reload(roundupdb)
172         #reload(htmltemplate)
173         #reload(cgi_client)
174         #sys.path.insert(0, module_path)
175         #try:
176         #    reload(instance)
177         #finally:
178         #    del sys.path[0]
180         self.send_response(200, "Script output follows")
182         # do the roundup thang
183         client = instance.Client(instance, self.wfile, env)
184         client.main()
186     do_POST = run_cgi
188 def usage(message=''):
189     if message: message = 'Error: %s\n'%message
190     print '''%sUsage:
191 roundup-server [-n hostname] [-p port] [name=instance home]*
193  -n: sets the host name
194  -p: sets the port to listen on
196  name=instance home
197    Sets the instance home(s) to use. The name is how the instance is
198    identified in the URL (it's the first part of the URL path). The
199    instance home is the directory that was identified when you did
200    "roundup-admin init". You may specify any number of these name=home
201    pairs on the command-line. For convenience, you may edit the
202    ROUNDUP_INSTANCE_HOMES variable in the roundup-server file instead.
203 '''%message
204     sys.exit(0)
206 def main():
207     hostname = ''
208     port = 8080
209     try:
210         # handle the command-line args
211         optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:')
212         user = ROUNDUP_USER
213         for (opt, arg) in optlist:
214             if opt == '-n': hostname = arg
215             elif opt == '-p': port = int(arg)
216             elif opt == '-u': user = arg
217             elif opt == '-h': usage()
219         if hasattr(os, 'getuid'):
220             # if root, setuid to the running user
221             if not os.getuid() and user is not None:
222                 try:
223                     import pwd
224                 except ImportError:
225                     raise ValueError, "Can't change users - no pwd module"
226                 try:
227                     uid = pwd.getpwnam(user)[2]
228                 except KeyError:
229                     raise ValueError, "User %s doesn't exist"%user
230                 os.setuid(uid)
231             elif os.getuid() and user is not None:
232                 print 'WARNING: ignoring "-u" argument, not root'
234             # People can remove this check if they're really determined
235             if not os.getuid() and user is None:
236                 raise ValueError, "Can't run as root!"
238         # handle instance specs
239         if args:
240             d = {}
241             for arg in args:
242                 try:
243                     name, home = string.split(arg, '=')
244                 except ValueError:
245                     raise ValueError, "Instances must be name=home"
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()
263 # $Log: not supported by cvs2svn $
264 # Revision 1.15  2001/10/12 02:23:26  richard
265 # Didn't clean up after myself :)
267 # Revision 1.14  2001/10/12 02:20:32  richard
268 # server now handles setuid'ing much better
270 # Revision 1.13  2001/10/05 02:23:24  richard
271 #  . roundup-admin create now prompts for property info if none is supplied
272 #    on the command-line.
273 #  . hyperdb Class getprops() method may now return only the mutable
274 #    properties.
275 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
276 #    now support anonymous user access (read-only, unless there's an
277 #    "anonymous" user, in which case write access is permitted). Login
278 #    handling has been moved into cgi_client.Client.main()
279 #  . The "extended" schema is now the default in roundup init.
280 #  . The schemas have had their page headings modified to cope with the new
281 #    login handling. Existing installations should copy the interfaces.py
282 #    file from the roundup lib directory to their instance home.
283 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
284 #    Ping - has been removed.
285 #  . Fixed a whole bunch of places in the CGI interface where we should have
286 #    been returning Not Found instead of throwing an exception.
287 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
288 #    an item now throws an exception.
290 # Revision 1.12  2001/09/29 13:27:00  richard
291 # CGI interfaces now spit up a top-level index of all the instances they can
292 # serve.
294 # Revision 1.11  2001/08/07 00:24:42  richard
295 # stupid typo
297 # Revision 1.10  2001/08/07 00:15:51  richard
298 # Added the copyright/license notice to (nearly) all files at request of
299 # Bizar Software.
301 # Revision 1.9  2001/08/05 07:44:36  richard
302 # Instances are now opened by a special function that generates a unique
303 # module name for the instances on import time.
305 # Revision 1.8  2001/08/03 01:28:33  richard
306 # Used the much nicer load_package, pointed out by Steve Majewski.
308 # Revision 1.7  2001/08/03 00:59:34  richard
309 # Instance import now imports the instance using imp.load_module so that
310 # we can have instance homes of "roundup" or other existing python package
311 # names.
313 # Revision 1.6  2001/07/29 07:01:39  richard
314 # Added vim command to all source so that we don't get no steenkin' tabs :)
316 # Revision 1.5  2001/07/24 01:07:59  richard
317 # Added command-line arg handling to roundup-server so it's more useful
318 # out-of-the-box.
320 # Revision 1.4  2001/07/23 10:31:45  richard
321 # disabled the reloading until it can be done properly
323 # Revision 1.3  2001/07/23 08:53:44  richard
324 # Fixed the ROUNDUPS decl in roundup-server
325 # Move the installation notes to INSTALL
327 # Revision 1.2  2001/07/23 04:05:05  anthonybaxter
328 # actually quit if python version wrong
330 # Revision 1.1  2001/07/23 03:46:48  richard
331 # moving the bin files to facilitate out-of-the-boxness
333 # Revision 1.1  2001/07/22 11:15:45  richard
334 # More Grande Splite stuff
337 # vim: set filetype=python ts=4 sw=4 et si