Code

- updated email package address formatting (deprecation)
[roundup.git] / roundup / scripts / roundup_server.py
1 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
2 # This module is free software, and you may redistribute it and/or modify
3 # under the same terms as Python, so long as this copyright message and
4 # disclaimer are retained in their original form.
5 #
6 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
7 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
8 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
9 # POSSIBILITY OF SUCH DAMAGE.
10 #
11 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
12 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
14 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16
17 """ HTTP Server that serves roundup.
19 $Id: roundup_server.py,v 1.15 2002-11-05 22:59:46 richard Exp $
20 """
22 # python version check
23 from roundup import version_check
25 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
26 import BaseHTTPServer
28 # Roundup modules of use here
29 from roundup.cgi import cgitb, client
30 import roundup.instance
31 from roundup.i18n import _
33 #
34 ##  Configuration
35 #
37 # This indicates where the Roundup trackers live. They're given as NAME ->
38 # TRACKER_HOME, where the NAME part is used in the URL to select the
39 # appropriate reacker.
40 # Make sure the NAME part doesn't include any url-unsafe characters like 
41 # spaces, as these confuse the cookie handling in browsers like IE.
42 TRACKER_HOMES = {
43     'bar': '/tmp/bar',
44 }
46 ROUNDUP_USER = None
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 #
62 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
63     TRACKER_HOMES = TRACKER_HOMES
64     ROUNDUP_USER = ROUNDUP_USER
66     def run_cgi(self):
67         """ Execute the CGI command. Wrap an innner call in an error
68             handler so all errors can be caught.
69         """
70         save_stdin = sys.stdin
71         sys.stdin = self.rfile
72         try:
73             self.inner_run_cgi()
74         except client.NotFound:
75             self.send_error(404, self.path)
76         except client.Unauthorised:
77             self.send_error(403, self.path)
78         except:
79             # it'd be nice to be able to detect if these are going to have
80             # any effect...
81             self.send_response(400)
82             self.send_header('Content-Type', 'text/html')
83             self.end_headers()
84             try:
85                 reload(cgitb)
86                 self.wfile.write(cgitb.breaker())
87                 self.wfile.write(cgitb.html())
88             except:
89                 self.wfile.write("<pre>")
90                 s = StringIO.StringIO()
91                 traceback.print_exc(None, s)
92                 self.wfile.write(cgi.escape(s.getvalue()))
93                 self.wfile.write("</pre>\n")
94         sys.stdin = save_stdin
96     do_GET = do_POST = do_HEAD = send_head = run_cgi
98     def index(self):
99         ''' Print up an index of the available trackers
100         '''
101         self.send_response(200)
102         self.send_header('Content-Type', 'text/html')
103         self.end_headers()
104         w = self.wfile.write
105         w(_('<html><head><title>Roundup trackers index</title></head>\n'))
106         w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
107         keys = self.TRACKER_HOMES.keys()
108         keys.sort()
109         for tracker in keys:
110             w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
111                 'tracker_url': urllib.quote(tracker),
112                 'tracker_name': cgi.escape(tracker)})
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 tracker
127         if rest == '/':
128             return self.index()
129         l_path = rest.split('/')
130         tracker_name = urllib.unquote(l_path[1])
131         if self.TRACKER_HOMES.has_key(tracker_name):
132             tracker_home = self.TRACKER_HOMES[tracker_name]
133             tracker = roundup.instance.open(tracker_home)
134         else:
135             raise 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['TRACKER_NAME'] = tracker_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['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
162         env['SCRIPT_NAME'] = ''
163         env['SERVER_NAME'] = self.server.server_name
164         env['SERVER_PORT'] = str(self.server.server_port)
165         env['HTTP_HOST'] = self.headers['host']
167         decoded_query = query.replace('+', ' ')
169         # do the roundup thang
170         c = tracker.Client(tracker, self, env)
171         c.main()
173 def usage(message=''):
174     if message:
175         message = _('Error: %(error)s\n\n')%{'error': message}
176     print _('''%(message)sUsage:
177 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=tracker home]*
179  -n: sets the host name
180  -p: sets the port to listen on
181  -l: sets a filename to log to (instead of stdout)
182  -d: daemonize, and write the server's PID to the nominated file
184  name=tracker home
185    Sets the tracker home(s) to use. The name is how the tracker is
186    identified in the URL (it's the first part of the URL path). The
187    tracker home is the directory that was identified when you did
188    "roundup-admin init". You may specify any number of these name=home
189    pairs on the command-line. For convenience, you may edit the
190    TRACKER_HOMES variable in the roundup-server file instead.
191    Make sure the name part doesn't include any url-unsafe characters like 
192    spaces, as these confuse the cookie handling in browsers like IE.
193 ''')%locals()
194     sys.exit(0)
196 def daemonize(pidfile):
197     ''' Turn this process into a daemon.
198         - make sure the sys.std(in|out|err) are completely cut off
199         - make our parent PID 1
201         Write our new PID to the pidfile.
203         From A.M. Kuuchling (possibly originally Greg Ward) with
204         modification from Oren Tirosh, and finally a small mod from me.
205     '''
206     # Fork once
207     if os.fork() != 0:
208         os._exit(0)
210     # Create new session
211     os.setsid()
213     # Second fork to force PPID=1
214     pid = os.fork()
215     if pid:
216         pidfile = open(pidfile, 'w')
217         pidfile.write(str(pid))
218         pidfile.close()
219         os._exit(0)         
221     os.chdir("/")         
222     os.umask(0)
224     # close off sys.std(in|out|err), redirect to devnull so the file
225     # descriptors can't be used again
226     devnull = os.open('/dev/null', 0)
227     os.dup2(devnull, 0)
228     os.dup2(devnull, 1)
229     os.dup2(devnull, 2)
231 def abspath(path):
232     ''' Make the given path an absolute path.
234         Code from Zope-Coders posting of 2002-10-06 by GvR.
235     '''
236     if not os.path.isabs(path):
237         path = os.path.join(os.getcwd(), path)
238     return os.path.normpath(path)
240 def run():
241     ''' Script entry point - handle args and figure out what to to.
242     '''
243     hostname = ''
244     port = 8080
245     pidfile = None
246     logfile = None
247     try:
248         # handle the command-line args
249         try:
250             optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:')
251         except getopt.GetoptError, e:
252             usage(str(e))
254         user = ROUNDUP_USER
255         for (opt, arg) in optlist:
256             if opt == '-n': hostname = arg
257             elif opt == '-p': port = int(arg)
258             elif opt == '-u': user = arg
259             elif opt == '-d': pidfile = abspath(arg)
260             elif opt == '-l': logfile = abspath(arg)
261             elif opt == '-h': usage()
263         if hasattr(os, 'getuid'):
264             # if root, setuid to the running user
265             if not os.getuid() and user is not None:
266                 try:
267                     import pwd
268                 except ImportError:
269                     raise ValueError, _("Can't change users - no pwd module")
270                 try:
271                     uid = pwd.getpwnam(user)[2]
272                 except KeyError:
273                     raise ValueError, _("User %(user)s doesn't exist")%locals()
274                 os.setuid(uid)
275             elif os.getuid() and user is not None:
276                 print _('WARNING: ignoring "-u" argument, not root')
278             # People can remove this check if they're really determined
279             if not os.getuid() and user is None:
280                 raise ValueError, _("Can't run as root!")
282         # handle tracker specs
283         if args:
284             d = {}
285             for arg in args:
286                 try:
287                     name, home = arg.split('=')
288                 except ValueError:
289                     raise ValueError, _("Instances must be name=home")
290                 d[name] = home
291             RoundupRequestHandler.TRACKER_HOMES = d
292     except SystemExit:
293         raise
294     except:
295         exc_type, exc_value = sys.exc_info()[:2]
296         usage('%s: %s'%(exc_type, exc_value))
298     # we don't want the cgi module interpreting the command-line args ;)
299     sys.argv = sys.argv[:1]
300     address = (hostname, port)
302     # fork?
303     if pidfile:
304         daemonize(pidfile)
306     # redirect stdout/stderr to our logfile
307     if logfile:
308         sys.stdout = sys.stderr = open(logfile, 'a')
310     httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
311     print _('Roundup server started on %(address)s')%locals()
312     httpd.serve_forever()
314 if __name__ == '__main__':
315     run()
317 # vim: set filetype=python ts=4 sw=4 et si